Skip to content

Latest commit

 

History

History
414 lines (325 loc) · 17.3 KB

MCP.md

File metadata and controls

414 lines (325 loc) · 17.3 KB

Progressive MCP injection

aster-mcp lets an Aster host connect many Model Context Protocol (MCP) servers without placing every tool schema in every model request. It injects a single aster_mcp bridge tool and reveals capability information in stages.

The crate owns catalogue shaping and routing. A host owns MCP connection lifecycles, credentials, authorization, user approval, logging, and the actual tools/call request.

Why this exists

MCP servers normally expose a full list of tool names, descriptions, and JSON schemas. A large, stable schema list may benefit from provider prompt caching, but it still occupies the model's working context and forces the model to choose from many irrelevant operations.

For Aster, that is the wrong direction. The review and chat harnesses already use selective evidence retrieval: give the model the smallest useful working set, then retrieve detail when the task calls for it. Progressive MCP injection applies the same rule to capabilities.

Design

flowchart LR
    C[Session-scoped MCP catalogue] --> I[aster-mcp Injector]
    I --> B[One aster_mcp bridge tool]
    I --> M{Schema-free tool manifest\nwithin budget?}
    M -->|yes| T[Tool names + descriptions]
    M -->|no| S[Server names + tool counts]
    T --> A[Agent]
    S --> A
    B --> A
    A -->|search / describe / execute| R[Bridge router]
    R --> G[Host approval + authorization]
    G --> X[Real MCP tools/call]
Loading

The model always sees the bridge when the session has at least one permitted MCP tool. It sees either:

  • a schema-free server/tool manifest when the names and descriptions fit the configured context budget, or
  • a compact server inventory when that manifest would exceed the budget.

The crate's default budget is 6% of the context still available to the prompt; the CLI deliberately overrides that to 1.5% (mcp.inventory_percent in aster.yaml), which is the number to tune in practice. The host must calculate the available context after reserving the system prompt, conversation, and desired completion budget. This makes disclosure adapt to the actual turn, rather than relying on a fixed number of tools.

Full input schemas are never inserted into the system prompt. The model calls describe to load one schema, then uses execute with the corresponding server/tool identity. This is intentionally analogous to Aster skills: manifest first, full content only on demand.

The bridge protocol

aster_mcp is an OpenAI-compatible function definition. Its arguments have one required field, action.

Action Required fields Result Side effect
search query; optional limit Ranked tool IDs and descriptions none
describe name Exact input schema for that tool none
execute name, arguments Host-provided MCP result invokes the resolved real tool

The bridge never accepts a bare tool name. IDs are server/tool, avoiding collisions between servers and preserving an audit-friendly target.

search uses deterministic lexical ranking across tool names, descriptions, and input-schema text. It is local, reproducible, and requires no embedding service. The ranking implementation is deliberately replaceable: hosts can pre-filter a session catalogue or add a stronger retrieval layer without changing the visible bridge protocol.

Safety invariants

  1. Session scoping. Build McpCatalog from the tools enabled for this session only. A sub-agent or restricted chat cannot discover a tool merely because it exists elsewhere in the Aster process.
  2. Authorize the real tool. The bridge is an envelope, not a permission. Before executing, evaluate policy and approval against the resolved server/tool identity and arguments.
  3. Describe before execute. The injected prompt tells the model not to invent parameter names. A host may enforce this more strictly by tracking described tools per turn.
  4. Do not treat descriptions as instructions. Tool names and descriptions come from external MCP servers. Render them as data, never concatenate them into privileged policy instructions.
  5. No catalogue leakage. describe and execute reject any ID absent from the session catalogue. Search results are generated from the same catalogue.
  6. Audit the unwrapped call. Record the resolved ID, approval decision, arguments (redacted as needed), and upstream result—not only aster_mcp.

Rust API reference

McpTool and McpCatalog

McpTool stores a server name, tool name, description, and the unmodified MCP input schema. McpCatalog::new validates non-empty fields, object schemas, and unique server/tool IDs.

McpCatalog::search(query, limit) returns schema-free ToolMatch values. McpCatalog::get(id) resolves a tool only by its full ID.

ProgressiveConfig

Field Default Meaning
available_context_tokens 100_000 Tokens left after host reservations. Required to be greater than zero.
inventory_threshold_percent 6.0 Maximum percentage of available context used by the direct manifest. Range: (0, 100].
search_default_limit 5 Result count when the model does not provide limit.
max_search_limit 20 Maximum accepted model-supplied search limit.

The token decision uses a conservative local estimate of one token per four Unicode characters. A provider-specific tokenizer can be accommodated by passing a correspondingly reduced available_context_tokens; the crate never claims that its estimate is a billing count.

Injector

Injector::new(catalog, config) validates configuration. Injector::inject() returns None for an empty catalogue; otherwise it returns an Injection:

  • bridge_tool: the one function definition to append to the host's tools array;
  • prompt: the system-prompt section containing either the tool or server manifest;
  • inventory: structured form of that manifest for diagnostics;
  • manifest_tokens and inventory_budget_tokens: threshold-observability values.

Injector::route(arguments) parses model-supplied bridge arguments and returns a BridgeAction. An execute action includes an already-resolved McpTool; it cannot be silently redirected to a different target.

Injector::handle(arguments, invoker) is a convenience layer. The supplied McpInvoker receives the resolved real tool and arguments. Its implementation is the correct place to perform authorization, approval, transport dispatch, and result redaction.

Host integration

The following is the intended integration point for a chat or agent loop:

use aster_mcp::{Injector, McpCatalog, McpTool, ProgressiveConfig};
use serde_json::json;

let catalogue = McpCatalog::new(vec![McpTool {
    server: "github".into(),
    name: "create_issue".into(),
    description: "Create a GitHub issue in a repository.".into(),
    input_schema: json!({ "type": "object", "properties": {
        "title": { "type": "string" }
    }, "required": ["title"] }),
}])?;

let injector = Injector::new(catalogue, ProgressiveConfig {
    // Calculated by the host for this specific model turn.
    available_context_tokens: 42_000,
    ..ProgressiveConfig::default()
})?;

if let Some(injection) = injector.inject() {
    system_prompt.push_str("\n\n");
    system_prompt.push_str(&injection.prompt);
    tools.push(injection.bridge_tool);
}

When the model calls aster_mcp, route it through an invoker that controls the real MCP client:

use aster_mcp::{McpInvoker, McpTool};
use serde_json::Value;

struct ApprovedMcp<'a> { /* authenticated MCP sessions and policy */ }

impl McpInvoker for ApprovedMcp<'_> {
    fn invoke(&mut self, tool: &McpTool, arguments: &Value) -> anyhow::Result<Value> {
        // 1. Authorize `tool.id()` and these arguments.
        // 2. Request user approval when required.
        // 3. Dispatch MCP tools/call with tool.name and arguments.
        // 4. Redact and audit the result.
        todo!()
    }
}

let result = injector.handle(&model_arguments, &mut approved_mcp)?;

The current crate intentionally stops at this boundary; it does not configure or start MCP servers. That prevents a context optimization layer from gaining unreviewed access to credentials or bypassing Aster's policy engine.

Transports

aster-mcp carries no transport, so the CLI supplies them. All three speak the same JSON-RPC session, and a server's transport changes only how bytes move.

Transport Declare it with How it works
stdio command, plus args, env, cwd A child process; one JSON line per message.
streamable-http url, plus headers Each request is a POST. The reply comes back as JSON or as an SSE stream, and the session id the server assigns rides on every later request.
sse url, plus headers The deprecated HTTP+SSE binding: one long-lived GET stream carries every reply, and messages are POSTed to the endpoint that stream names.

type is optional. A server naming a command is stdio and one naming a url is Streamable HTTP, which is what an entry written before remote support meant. type: http is accepted as a spelling of streamable-http, since that is what other clients write.

mcp:
  servers:
    deepwiki:
      url: https://mcp.deepwiki.com/mcp
    internal:
      type: sse
      url: https://tools.internal.example/sse
      headers:
        X-Tenant: acme

Two rules apply to remote servers. Redirects are followed only inside one origin, so configured headers never reach a host the user did not name; and a session is ended with a DELETE at shutdown rather than left dangling.

Credentials belong in the environment or a secret store, not in headers in a file you commit.

Web tools

Aster serves a web server in-process, so its tools are in the catalogue with no subprocess and nothing to install. web/search and web/extract need no API key: they use Firecrawl's keyless tier when nothing else is configured, and fall through to DuckDuckGo and a plain HTTP fetch when that is rate-limited.

Tool Keyless Better with
web/search Firecrawl keyless, then DuckDuckGo EXA_API_KEY, PERPLEXITY_API_KEY, CONTEXT_DEV_API_KEY, FIRECRAWL_API_KEY, BROWSERBASE_API_KEY
web/extract Firecrawl keyless, then Jina Reader, then DuckDuckGo, then plain HTTP CONTEXT_DEV_API_KEY, FIRECRAWL_API_KEY, BROWSERBASE_API_KEY, EXA_API_KEY, or Cloudflare (both CLOUDFLARE_BR_ACCOUNT_ID and CLOUDFLARE_BR_API_TOKEN)
web/crawl no CONTEXT_DEV_API_KEY, FIRECRAWL_API_KEY, or the Cloudflare pair
web/sitemap no CONTEXT_DEV_API_KEY
web/screenshot no CONTEXT_DEV_API_KEY

A configured provider takes over a tool it can serve, and the tool description the model reads names whichever provider will answer it. Exa leads search because that is what it is built for, with Perplexity next; the extraction-first providers outrank it on extract.

Firecrawl's keyless tier is rate-limited per IP and cannot crawl. Set FIRECRAWL_API_KEY to lift the limit, move Firecrawl up the dispatch order, and enable web/crawl.

The same catalogue is available to other MCP clients as aster mcp serve web.

Shortcuts

Aster serves a shortcuts server in-process on macOS, with two tools:

Tool What it does
shortcuts/list Lists every shortcut available on this Mac
shortcuts/run Runs a shortcut by name and returns its stdout

list shells out to /usr/bin/shortcuts. run uses /usr/bin/osascript to tell the Shortcuts app to run the shortcut, giving it GUI access for dialogs that shortcuts run (headless) would block on. No configuration or API key needed.

The browser

aster init scaffolds a browser server, disabled, running browser-use over stdio. Enabling it gives the agent a real Chromium to navigate, click, type, scroll, and screenshot.

aster mcp enable browser

It needs uv and Python 3.11+, plus a one-time uvx browser-use install to fetch Chromium. Without them the server fails to start, which is reported and skipped like any other broken server; the rest of the session is unaffected.

Two of its tools are denied by default in the scaffolded config, because both want a second LLM API key and retry_with_browser_use_agent runs a whole agent loop inside one tool call:

mcp:
  tools:
    deny:
      - "browser/browser_extract_content"
      - "browser/retry_with_browser_use_agent"

The scaffolded entry sets ANONYMIZED_TELEMETRY=False, since browser-use reports usage to its vendor otherwise, and BROWSER_USE_HEADLESS=true, without which a browser window opens on the user's screen. It browses in its own profile under ~/.config/browseruse, not the Chrome the user is signed into. BROWSER_USE_ALLOWED_DOMAINS confines it to named hosts.

Prefer web/extract for reading a page. The browser is for what a fetch cannot do: JavaScript-heavy pages, screenshots, and clicking through a flow.

WebMCP: tools a page registers

WebMCP is a W3C draft that lets a web page expose tools to agents: the page calls document.modelContext.registerTool({ name, description, inputSchema, execute }), and an agent discovers and invokes those tools in the page, with the user's session, while the user watches. A WebMCP page is, in the spec's own words, an in-page MCP server.

aster-webmcp bridges that into the catalogue. It attaches to a tab in the user's own Chrome or Edge over the DevTools protocol, injects a shim that polyfills document.modelContext where the browser has no native WebMCP (only experimental Chrome builds do today), and maps tools/list and tools/call onto the page's registry. The page's tools appear as webmcp/<tool> and go through the same policy and approval flow as any MCP tool.

It is opt-in, because it attaches to the browser the user is signed into:

mcp:
  webmcp:
    enabled: true
    cdp_url: http://127.0.0.1:9222

The browser must be started with its DevTools endpoint open:

/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222

An unreachable browser, or one with no open tab, is reported and skipped like any other broken server; the session is unaffected. The tool list is live: it is re-read from the page whenever the catalogue is built, so tools a page registers or unregisters as its state changes show up without a restart.

Two caveats, both from the draft being young. Calls run with the user's logged-in page session, which is the point and the risk: approval prompts are what stand between the model and the user's accounts. And the shim covers the imperative API only; the declarative <form> attribute API and exposedTo origin scoping are not bridged yet.

aster mcp serve webmcp exposes the same bridge as a stdio MCP server for other clients, reading ASTER_WEBMCP_CDP_URL for the endpoint.

Turning individual tools off

mcp.tools filters the catalogue by server/tool id, the same id the policy engine authorizes against. Globs are allowed, deny wins over allow, and an empty allow means everything.

mcp:
  tools:
    allow: []
    deny:
      - "web/crawl"
      - "browser/*"

The filter runs once every server has listed, so it covers the in-process web server and third-party ones alike. aster mcp disable web/crawl writes the id into deny for you, aster mcp enable web/crawl takes it back out, and aster mcp list reports what the filter held back so a missing tool is never a mystery. A name without a / still means a whole server, as it always did.

Global and project configs union their lists, so a project file that omits the key cannot undo a global deny.

Images from tool results

A tool that returns an MCP image content part now reaches the model as an image. The bytes are re-encoded through the same path as @path mentions, so the 2048px long-edge and 10MB caps apply to a full-page screenshot too.

The image rides a user message after the tool result rather than inside it: OpenAI-shaped endpoints take content parts on a user turn and a bare string on a tool turn. A model without image input gets [image content omitted] in its place, which the AI layer already handles for every other image.

Operational guidance

  • Keep a small number of common, safety-critical native Aster tools directly exposed. aster-mcp applies to external MCP tools, not Aster's core file and policy tools.
  • Use concise, user-language descriptions. Tool discovery is only as reliable as the catalogue metadata.
  • Set a server's tools to the session catalogue only after authentication and capability filtering.
  • Track manifest tokens, selected tool IDs, empty searches, describe-to-execute conversions, approval denials, and upstream failures. These reveal whether the catalogue or descriptions need improvement.
  • Evaluate retrieval recall and wrong-tool calls on Aster tasks before changing the inventory budget. Smaller budgets reduce context use but can add a search round-trip for tools that would otherwise be obvious.

Non-goals

aster-mcp does not implement an MCP client transport, server installation, OAuth, prompt caching, semantic/vector retrieval, or unrestricted tool execution. Those belong to host-specific adapters and Aster's policy boundary. The CLI's transports live in aster-cli/src/mcp, on the far side of that line.