Home Docs Blog Demo

MCP Agents

Define named personas with system prompts, scoped tools, and dynamic context injection.

Agents are named personas that combine a system prompt, a scoped set of tools, and optional dynamic context injection. Instead of explaining what you want in every conversation, you define an agent once and activate it by name.

Why agents?

Without agents, every conversation starts from zero:

User: "You are a code reviewer. Search for our coding standards..."

With agents, the workflow becomes:

User: "Review this PR" (using the code-reviewer agent)

The agent pre-configures:

Three definition modes

ModeConfigBest for
Inline TOML[agents.inline.<name>]Static prompts, simple agents
Lua script[agents.script.<name>]Dynamic context injection, conditional logic
Rust traitimpl Agent for MyAgentCompiled extensions in custom binaries

Inline TOML agents

The simplest way to define an agent — everything in ctx.toml:

[agents.inline.code-reviewer]
description = "Reviews code changes against project conventions"
tools = ["search", "get"]
system_prompt = """
You are a senior code reviewer for this project. When reviewing code:
1. Use the `search` tool to find relevant coding conventions and patterns
2. Use the `get` tool to read full documents when snippets aren't enough
3. Be specific — cite which convention a suggestion relates to
4. Suggest improvements, not just problems

Always ground your feedback in the project's documented standards.
"""

[agents.inline.architect]
description = "Answers architecture questions using indexed documentation"
tools = ["search", "get", "sources"]
system_prompt = """
You are a software architect with deep knowledge of this codebase.
Use the search tool to find architecture decision records (ADRs),
design documents, and relevant code patterns. Always cite your sources.
When recommending changes, explain tradeoffs clearly.
"""

These agents appear immediately in GET /agents/list and can be resolved via POST /agents/{name}/prompt.


Lua scripted agents

For agents that need dynamic context injection — pre-searching the knowledge base before the conversation starts:

[agents.script.incident-responder]
path = "agents/incident-responder.lua"
timeout = 30
search_limit = 5
-- agents/incident-responder.lua

agent = {}

agent.name = "incident-responder"
agent.description = "Helps triage production incidents with relevant runbooks"
agent.tools = { "search", "get", "create_jira_ticket" }

-- Arguments the user can provide
agent.arguments = {
    {
        name = "service",
        description = "The service experiencing the incident",
        required = false,
    },
    {
        name = "severity",
        description = "Incident severity (P1, P2, P3)",
        required = false,
    },
}

function agent.resolve(args, config, context)
    local service = args.service or "unknown"
    local severity = args.severity or "P2"

    -- Pre-search for relevant runbooks
    local results = context.search(
        service .. " incident runbook",
        { mode = "keyword", limit = config.search_limit or 5 }
    )

    -- Fetch full content and inject as context
    local runbook_text = ""
    for _, r in ipairs(results) do
        local doc = context.get(r.id)
        runbook_text = runbook_text .. "\n\n## " .. doc.title .. "\n" .. doc.body
    end

    return {
        system = string.format([[
You are an incident responder for the %s service (%s severity).
You have access to the following runbooks:
%s

Use the search tool for additional context.
Use create_jira_ticket when a tracking ticket is needed.
Be methodical: gather context, identify the issue, recommend actions.
        ]], service, severity, runbook_text),

        -- Inject a starter message
        messages = {
            {
                role = "assistant",
                content = string.format(
                    "I'm ready to help with the %s %s incident. "
                    .. "I've loaded %d relevant runbooks. What's the current situation?",
                    severity, service, #results
                ),
            },
        },
    }
end

return agent

The context bridge provides:

FunctionDescription
context.search(query, opts?)Search the knowledge base (keyword/semantic/hybrid)
context.get(id)Retrieve a full document by UUID
context.sources()List all data sources and their status
context.configTool config from ctx.toml (env vars expanded)

HTTP endpoints

GET /agents/list

Discover all registered agents:

$ curl -s localhost:7331/agents/list | jq '.agents[] | {name, description, tools}'
{
  "name": "code-reviewer",
  "description": "Reviews code changes against project conventions",
  "tools": ["search", "get"]
}
{
  "name": "incident-responder",
  "description": "Helps triage production incidents with relevant runbooks",
  "tools": ["search", "get", "create_jira_ticket"]
}

POST /agents/{name}/prompt

Resolve an agent’s prompt (for Lua agents, this executes agent.resolve()):

$ curl -s localhost:7331/agents/incident-responder/prompt \
    -H "Content-Type: application/json" \
    -d '{"service": "payments-api", "severity": "P1"}' | jq .
{
  "system": "You are an incident responder for the payments-api service (P1 severity)...",
  "tools": ["search", "get", "create_jira_ticket"],
  "messages": [
    {
      "role": "assistant",
      "content": "I'm ready to help with the P1 payments-api incident..."
    }
  ]
}
StatusMeaning
200Success
404Agent not found
500Lua resolve() failed
408Lua resolve() timed out

CLI commands

# List all configured agents
$ ctx agent list
  code-reviewer        Reviews code changes against project conventions   (tools: search, get)        [toml]
  architect            Answers architecture questions using indexed docs   (tools: search, get, sources) [toml]
  incident-responder   Helps triage production incidents with runbooks     (tools: search, get, create_jira_ticket) [lua]

# Test a Lua agent with arguments
$ ctx agent test incident-responder --arg service=payments-api --arg severity=P1

Agent: incident-responder
Source: lua (agents/incident-responder.lua)
Tools: search, get, create_jira_ticket

System prompt (487 chars):
  You are an incident responder for the payments-api service (P1 severity).
  ...

Messages (1):
  [assistant] I'm ready to help with the P1 payments-api incident...

# Scaffold a new Lua agent
$ ctx agent init sre-helper
Created: agents/sre-helper.lua
Add to config:

  [agents.script.sre-helper]
  path = "agents/sre-helper.lua"
  timeout = 30

Using agents with Cursor

Once your agents are configured and the MCP server is running, you can activate agents in Cursor conversations.

Step 1: Start the server with agents:

$ ctx serve mcp --config ./config/ctx.toml
Registered 6 tools:
  POST /tools/search — Search indexed documents (builtin)
  POST /tools/get — Get document by ID (builtin)
  POST /tools/sources — List data sources (builtin)
Registered 3 agents:
  POST /agents/code-reviewer/prompt — Reviews code changes (toml)
  POST /agents/architect/prompt — Answers architecture questions (toml)
  POST /agents/incident-responder/prompt — Helps triage incidents (lua)
MCP server listening on http://127.0.0.1:7331

Step 2: Resolve an agent’s prompt and use it:

The simplest integration: call POST /agents/{name}/prompt to get the system prompt, then use it in your LLM conversation. The agent’s tools array tells you which Context Harness tools to make available.

Step 3: In Cursor, the agent pattern works naturally:


SDLC agent examples

Here’s a set of agents that cover the full software development lifecycle:

# config/ctx.toml

# ── Development ──────────────────────────────────
[agents.inline.code-reviewer]
description = "Reviews code against project conventions and patterns"
tools = ["search", "get"]
system_prompt = """..."""

[agents.inline.architect]
description = "Answers architecture questions using indexed ADRs and design docs"
tools = ["search", "get", "sources"]
system_prompt = """..."""

# ── Operations ───────────────────────────────────
[agents.inline.sre-responder]
description = "Helps triage production incidents with runbooks and context"
tools = ["search", "get", "sources"]
system_prompt = """..."""

[agents.inline.release-manager]
description = "Helps with release planning, changelogs, and deployment"
tools = ["search", "get", "sources"]
system_prompt = """..."""

# ── Knowledge ────────────────────────────────────
[agents.inline.onboarding]
description = "Guides new engineers through the codebase"
tools = ["search", "get", "sources"]
system_prompt = """..."""

[agents.inline.tech-writer]
description = "Writes documentation matching project style"
tools = ["search", "get"]
system_prompt = """..."""

# ── Domain Experts (Lua, dynamic) ────────────────
[agents.script.domain-expert]
path = "agents/domain-expert.lua"
timeout = 30
search_limit = 10

Custom Rust agents

For compiled agents in custom harness binaries, implement the Agent trait:

use context_harness::{Agent, AgentPrompt, AgentArgument};
use context_harness::traits::ToolContext;
use async_trait::async_trait;
use serde_json::Value;
use anyhow::Result;

pub struct DatabaseExpert;

#[async_trait]
impl Agent for DatabaseExpert {
    fn name(&self) -> &str { "db-expert" }
    fn description(&self) -> &str { "Database design and query optimization" }
    fn tools(&self) -> Vec<String> { vec!["search".into(), "get".into()] }

    fn arguments(&self) -> Vec<AgentArgument> {
        vec![AgentArgument {
            name: "database".into(),
            description: "Target database name".into(),
            required: false,
        }]
    }

    async fn resolve(&self, args: Value, ctx: &ToolContext) -> Result<AgentPrompt> {
        let db = args["database"].as_str().unwrap_or("main");

        // Pre-search for schema documentation
        let results = ctx.search("database schema", None, None, None).await?;
        let context = results.iter()
            .map(|r| format!("- {}", r.title.as_deref().unwrap_or("?")))
            .collect::<Vec<_>>().join("\n");

        Ok(AgentPrompt {
            system: format!(
                "You are a database expert for '{}'.\nRelevant docs:\n{}",
                db, context
            ),
            tools: self.tools(),
            messages: vec![],
        })
    }
}

Register it in your custom binary:

let mut agents = AgentRegistry::new();
agents.register(Box::new(DatabaseExpert));
run_server_with_extensions(config, tools, Arc::new(agents)).await?;

See the full example.


What’s next?