diff --git a/Home.md b/Home.md index 84598d0..838be3b 100644 --- a/Home.md +++ b/Home.md @@ -35,7 +35,7 @@ Coming from [AIChat](https://github.com/sigoden/aichat)? Follow the [migration g * [Create Custom TypeScript Tools](Custom-Tools#custom-typescript-based-tools) * [Create Custom Bash Tools](Custom-Bash-Tools) * [Bash Prompt Utilities](Bash-Prompt-Helpers) -* [First-Class MCP Server Support](MCP-Servers): Easily connect and interact with MCP servers for advanced functionality. +* [First-Class MCP Server Support](MCP-Servers): Easily connect and interact with MCP servers for advanced functionality. Coyote supports all three MCP capabilities: tools, resources, and prompts, with capability-gated meta-tools for the model and a `.prompt` REPL command for invoking server prompts yourself. * [Macros](Macros): Automate repetitive tasks and workflows with Coyote "scripts" (macros), and invoke them as your own custom REPL commands. * [RAG](RAG): Retrieval-Augmented Generation for enhanced information retrieval and generation. * [Sessions](Sessions): Manage and persist conversational contexts and settings across multiple interactions. diff --git a/MCP-Servers.md b/MCP-Servers.md index 8ee265f..dff4922 100644 --- a/MCP-Servers.md +++ b/MCP-Servers.md @@ -544,6 +544,144 @@ To pick up new default servers added in a Coyote update, run `coyote --install m in the REPL). This **merges** the bundled template into your existing configuration: only servers not already present in your file are added; your existing servers and any custom secret references are left untouched. +# Interacting with MCP Servers + +Coyote does not flood the model's context with a separate function declaration for every tool an MCP server exposes. +Instead, each enabled server contributes a small set of **meta-functions** the model uses to discover and invoke the +server's capabilities on demand: + +| Meta-function | Purpose | +|-------------------------|------------------------------------------------------------------------------------------------| +| `mcp_search_` | Keyword search over the server's catalog of tools, resources, resource templates, and prompts | +| `mcp_describe_` | Fetch the full schema or metadata for exactly one catalog item | +| `mcp_invoke_` | Call a tool on the server | +| `mcp_read_` | Read a resource (or expand a resource template) from the server | +| `mcp_prompt_` | Fetch a server-defined prompt, rendered with the given arguments | + +This keeps the per-server context cost small and constant no matter how large the server is: the model searches for +what it needs, describes the matching item to get its exact schema, and then invokes, reads, or fetches it. + +## The Unified Catalog + +`mcp_search_` searches a single catalog spanning everything the server advertises: tools, resources, resource +templates, and prompts. Every search result carries a `kind` field (`tool`, `resource`, `resource_template`, or +`prompt`) so the model knows whether to follow up with `mcp_invoke`, `mcp_read`, or `mcp_prompt`. Listings follow +server-side pagination, so large servers are cataloged completely. + +`mcp_describe_` accepts the same `kind` as an optional parameter (default: `tool`). The `tool` parameter +carries the identifier for every kind: a tool name, a resource URI, a template's URI template, or a prompt name. + +* `kind: "tool"` returns the tool's full invocation schema +* `kind: "resource"` returns the resource's catalog metadata (URI, MIME type, size) +* `kind: "resource_template"` returns the template and its variables +* `kind: "prompt"` returns the prompt's name, description, and arguments + +Servers can annotate resources with an intended `audience` (`user` and/or `assistant`). Per the MCP spec this is +advisory metadata, not access control, and Coyote surfaces it on both search results and read results so the model +(and you) can see who the content was meant for. + +## Capability Gating + +Servers advertise which capabilities they support (tools, resources, prompts) during the connection handshake, and +Coyote only emits the meta-functions that make sense for each server: + +| Meta-function | Emitted when... | +|----------------|------------------------------------------------------------------------------------------------------------------------------------| +| `mcp_search` | Always (the catalog degrades per kind, so search works with whatever the server supports) | +| `mcp_describe` | Always (same reason) | +| `mcp_invoke` | The server advertises tools, **or** the handshake info is unavailable (fail-open: a handshake hiccup never strips a working server's tools) | +| `mcp_read` | The server advertises resources | +| `mcp_prompt` | The server advertises prompts | + +So a resources-only server still gets `mcp_search`, `mcp_describe`, and `mcp_read`, and never wastes context on an +`mcp_invoke` or `mcp_prompt` that would always error. + +## Reading Resources + +`mcp_read_` reads a resource by URI, or expands a resource template with variable values: + +| Parameter | Required | Description | +|-------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `uri` | yes | The resource URI, or a resource template with `{var}` placeholders | +| `arguments` | no | Template variable values. Only [RFC 6570](https://www.rfc-editor.org/rfc/rfc6570) Level 1 simple substitution is supported; values are percent-encoded | +| `pattern` | no | A regex applied to text content: only matching lines are returned, grep-style, with 2 lines of context and line-number prefixes | +| `offset` | no | Byte offset for paging text (default: `0`). When `pattern` is set, offsets refer to the **filtered** stream, not the raw resource | +| `max_bytes` | no | Maximum text bytes to return (default: `51200`, clamped to `204800`) | + +Text content comes back as a structured result with paging metadata: + +```json +{ + "uri": "file:///var/log/app.log", + "mime_type": "text/plain", + "text": "...", + "truncated": true, + "total_bytes": 1048576, + "next_offset": 51200, + "note": "Content truncated; re-call with offset=51200 to continue (max_bytes is clamped to 204800)" +} +``` + +The `pattern` filter is applied before slicing, so paging walks the filtered stream: `total_bytes` and `next_offset` +describe the filtered content, and for a multi-megabyte log resource, "lines matching ERROR" is one call instead of +dozens of pages. When a read returns multiple content items, the paging parameters apply per text item and the whole +response is additionally capped at 204800 bytes; items beyond the cap are replaced with a marker naming how many were +omitted. + +### Binary Content + +Binary content is **never** inlined into model context. Instead: + +1. Coyote first attempts a UTF-8 decode of the blob. If it decodes cleanly, it is treated as text and paged inline + regardless of the server's claimed MIME type (servers mislabel text constantly). +2. Genuinely binary content is decoded (up to a 50 MiB ceiling) and written to + `/mcp-resources//.`, with `0600` permissions on POSIX systems. Writes land in a + temp file and are renamed into place, so a visible spill file is always complete, even with concurrent readers. + The read returns a self-describing metadata object instead of the bytes: + +```json +{ + "spilled": true, + "path": "/home/you/.cache/coyote/mcp-resources/github/3b0c44...98fb.png", + "uri": "resource://github/chart", + "mime_type": "image/png", + "sniffed": false, + "size_bytes": 204812, + "sha256": "3b0c44...98fb" +} +``` + +The path is actionable by you, by `execute_command`, by the `fs_*` tools, and by sub-agents, which is more useful +than base64 the model cannot act on anyway. The spill directory is bounded: when the `mcp-resources/` tree exceeds +512 MiB, the oldest files are evicted best-effort. Files are content-addressed by SHA-256, so re-reading the same +content regenerates the identical path. Spilled files are untrusted input: Coyote never executes or auto-opens them. + +## Bounded Tool Results + +The same content policy bounds `mcp_invoke` tool results before they reach the model: + +* Text content larger than 204800 bytes is sliced at a UTF-8 boundary with a truncation note asking the model to + re-call with narrower arguments. +* Image, audio, and embedded binary content is routed through the same spill pipeline as resource reads: written to + disk, with the metadata object returned in its place. +* Oversized structured content (beyond the same 204800-byte ceiling once serialized) is replaced with a truncation + marker. +* Server-supplied metadata strings (URIs, MIME types, and similar) are clamped to 4096 bytes. + +In-bounds results pass through byte-identical, so well-behaved servers are unaffected. + +## Prompts + +Servers that advertise the prompts capability expose named, parameterized message templates rendered server-side. +Coyote surfaces them in two places: + +* **For you:** the [`.prompt` REPL command](REPL#prompt---invoke-an-mcp-server-prompt) invokes a prompt and submits + the result as your chat input, and `.list prompts` lists every prompt across enabled servers. +* **For the model:** the `mcp_prompt_` meta-function takes a `prompt` name and an optional `arguments` object + (string values only; prompt arguments have no schemas per the MCP spec) and returns the rendered prompt text as the + tool result. Multi-message prompts are flattened into a single block with `[user]` / `[assistant]` labels, and + missing required arguments produce an error listing them. + # Coyote Configuration MCP servers, like tools, can be used in a handful of contexts: * Inside a session @@ -553,6 +691,9 @@ MCP servers, like tools, can be used in a handful of contexts: Each of these has a different configuration and interaction with the global configuration. +Enabling a server in any of these contexts exposes its capability-gated meta-functions to the model; see +[Interacting with MCP Servers](#interacting-with-mcp-servers) above. + ***Note:** The names of each MCP server referenced in the below configuration properties directly corresponds to the names given in the `functions/mcp.json` configuration file. So if you change the name of an MCP server from `slack` to `lucem-slack`, then you need to also update your Coyote configuration accordingly. diff --git a/REPL.md b/REPL.md index b0f1c60..081cf18 100644 --- a/REPL.md +++ b/REPL.md @@ -18,6 +18,10 @@ things like * `.set key ` to complete configuration values * `.mcp auth ` to complete remote MCP server names * `.macro ` to complete macro names and the `enable`/`disable` subcommands + * `.list ` to complete the listable kinds (`roles`, `sessions`, `agents`, `rags`, `macros`, `skills`, + `prompts`, `tools`, `mcp-servers`, `bundles`) + * `.prompt ` to complete MCP servers, then prompt names, then `key=` arguments, queried live from the + running servers (see [`.prompt`](#prompt---invoke-an-mcp-server-prompt) below) * **Multi-Line Prompts:** You can also type prompts that span more than one line to help organize your thoughts. This can be done in the following ways: * `Ctrl-o` to open the current input buffer in your preferred editor (either the value of `editor` or `$EDITOR`) @@ -71,12 +75,62 @@ Coyote offers the following commands to manage your roles: For more information about roles in Coyote and how to build them, refer to the [roles documentation](Roles). -## `.prompt` - Set a temporary role using a prompt -If you need to create a temporary role that you want to discard after use, you use `.prompt`. `.prompt`-based roles -cannot be persisted to a file and saved. +## `.temp-role` - Set a temporary role using a prompt +If you need to create a temporary role that you want to discard after use, you use `.temp-role`. `.temp-role`-based +roles cannot be persisted to a file and saved. + +> **Renamed from `.prompt`:** this command used to be called `.prompt`. That name now invokes MCP server prompts +> (see [`.prompt`](#prompt---invoke-an-mcp-server-prompt) below). If you have muscle memory or macros built around +> `.prompt `, update them to `.temp-role `. ![prompt-role](./images/roles/prompt-role.gif) +## `.prompt` - Invoke an MCP server prompt +MCP servers can expose [prompts](MCP-Servers#prompts): named, parameterized message templates that are rendered +server-side, so the server can embed live data the client never sees. `.prompt` invokes one and submits the rendered +result as your chat input: + +``` +.prompt [key=value ...] +``` + +For example: + +``` +.prompt github summarize_pr repo=coyote number=42 +``` + +* **Arguments are named**, matching the prompt's declared argument names (MCP prompt arguments have no positional + order). Values containing spaces can be quoted: `key="some value"`. +* **Missing required arguments are prompted for interactively**, one at a time, before the prompt is fetched. +* **The result is submitted as your chat input**, exactly as if you had typed it. Multi-message prompts are flattened + into a single user message with `[user]` / `[assistant]` labels marking each original message's role. The flattened + text is never parsed as a REPL command or shell line, so prompt content beginning with `.` or `!` is chatted + verbatim, not executed. +* **Authentication errors surface here.** If the server's token has expired (or it was never authenticated), + `.prompt` reports the auth-required error the same way `mcp_invoke` does; run `.mcp auth ` to recover. + +Discover available prompts with `.list prompts`, which prints a `server / name / description / args` table across +all enabled servers. + +**Tab completion** is staged and queries the running MCP servers live on every TAB press (bounded by a 2-second +timeout per query): + +| Input | Suggestions | +|----------------------------------|-----------------------------------------------------------------------------------------------| +| `.prompt ` | Enabled, running servers that advertise the prompts capability | +| `.prompt ` | That server's prompt names, with descriptions | +| `.prompt ` | `key=` suggestions for the prompt's arguments, with required ones marked; keys you've already typed are excluded | + +An empty completion list is silent by design: it means the server is not running or authenticated, or has no +prompts, and is not a bug. Completion never starts a server and never triggers an authentication flow; an +enabled-but-unauthenticated (or stopped) server simply shows nothing at TAB. Auth recovery happens at invocation, +where `.prompt` surfaces the actual error. + +> **Macro shadowing:** built-in commands win name collisions, so a user macro named `prompt` is shadowed by this +> command. `.list macros` marks it `shadowed (built-in)`, and you can still run it explicitly with +> `.macro prompt [args...]`. + ## `.skill` - Skill management Skills are modular knowledge or capability packs the LLM can load and unload mid-conversation. Multiple skills can be loaded at once; their instructions stack and their tools/MCP servers union with the active role/agent/session. @@ -400,6 +454,7 @@ The `.list` command lists the assets of a given kind, making them discoverable w | `.list macros` | List all macros | | `.list bundles` | List installed bundles with their source, version, ref pin, and drift status (see [Sharing Configurations](Sharing-Configurations)) | | `.list skills` | List skills available in this context, with descriptions and a `(loaded)` marker for active skills | +| `.list prompts` | List MCP prompts across all enabled servers (live listing), with each prompt's server, description, and arguments | | `.list tools` | List the tools that can be enabled/disabled via `.tool [enable\|disable] ` (excludes internal tools; in an agent context, lists the agent's tool pool) | | `.list mcp-servers` | List the MCP servers that can be enabled/disabled via `.mcp [enable\|disable] ` (configured servers plus mapping aliases) | diff --git a/Roles.md b/Roles.md index 3f0de90..a86f5f2 100644 --- a/Roles.md +++ b/Roles.md @@ -258,7 +258,8 @@ for more examples. * `slack`: Interact with Slack using natural language # Temporary Roles -Coyote also enables you to create temporary roles that will be discarded once you're finished with them. This is done via -the `.prompt/--prompt` command: +Coyote also enables you to create temporary roles that will be discarded once you're finished with them. This is done via +the `.temp-role` REPL command (previously named `.prompt`; that name now invokes [MCP server prompts](REPL#prompt---invoke-an-mcp-server-prompt)) +or the `--prompt` CLI flag: ![prompt role](./images/roles/prompt-role.gif) diff --git a/Tools.md b/Tools.md index ad9e9c7..c41b071 100644 --- a/Tools.md +++ b/Tools.md @@ -204,6 +204,15 @@ the model receives: This allows the model to understand that an external service failed and take appropriate action (retry, use an alternative approach, or inform the user). +MCP error text shown in your terminal is sanitized (escape sequences are stripped so a misbehaving server cannot +drive your terminal); the JSON payload the model receives keeps the raw message. + +## MCP Tool Result Bounding +MCP tool results are bounded before they reach the model: text larger than 200 KiB is sliced with a truncation +marker, images and binary blobs are written to disk (with a metadata object returned in their place) instead of +being inlined as base64, and oversized structured content is replaced with a truncation marker. In-bounds results +pass through unchanged. See [MCP Servers: Bounded Tool Results](MCP-Servers#bounded-tool-results) for details. + ## Why This Matters Without proper error propagation, models would only know that "something went wrong" without understanding *what* went wrong. By including stderr output and detailed error messages, models can: diff --git a/_Sidebar.md b/_Sidebar.md index 2f2ff69..1577de2 100644 --- a/_Sidebar.md +++ b/_Sidebar.md @@ -36,6 +36,9 @@ - [Managing from CLI](MCP-Servers#managing-mcp-servers-from-the-cli) - [Workspace-Local Servers](MCP-Servers#workspace-local-mcp-servers) - [OAuth Authentication](MCP-Servers#oauth-authentication) + - [Interacting with MCP Servers](MCP-Servers#interacting-with-mcp-servers) + - [Reading Resources](MCP-Servers#reading-resources) + - [Prompts](MCP-Servers#prompts) - [Sandbox Compatibility](MCP-Servers#sandbox-compatibility) ## Agents