Skip to content

feat(mcp): add resources and prompts support - #1172

Merged
will-lamerton merged 4 commits into
Nano-Collective:mainfrom
addyCooks:feat/mcp-resources-prompts
Sep 15, 2026
Merged

will-lamerton merged 4 commits into
Nano-Collective:mainfrom
addyCooks:feat/mcp-resources-prompts

Conversation

@addyCooks

Copy link
Copy Markdown
Contributor

Closes #1162.

Description

Adds MCP resources and prompts support to MCPClient, and wires both into the interactive UI: resources join the existing @-mention/file-completion system, prompts dispatch as /mcp:<server>:<prompt> slash commands that feed the model the same way a custom command does. Phases 1 and 2 of #1162; sampling/elicitation/roots (phase 3) are explicitly out of scope for this PR.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

Changeset

  • Added a changeset (pnpm changeset) describing this change for the changelog

Testing

Automated Tests

  • New features include passing tests in .spec.ts/tsx files
  • All existing tests pass (pnpm test:all completes successfully)
  • Tests cover both success and error scenarios

Manual Testing

  • Tested with Ollama
  • Tested with OpenRouter
  • Tested with OpenAI-compatible API
  • Tested MCP integration (if applicable)

Checklist

  • If this was for an open issue, I was assigned to it
  • Code follows project style guidelines
  • Self-review completed
  • Documentation updated (if needed)
  • No breaking changes (or clearly documented)
  • Appropriate logging added using structured logging (see CONTRIBUTING.md)

@will-lamerton will-lamerton left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work, and thanks for keeping sampling/elicitation out of scope. Verified locally: tsc clean, biome clean, knip clean, all 88 tests in the touched specs pass. A few things to fix before this can go in.

Blocking

1. The branch doesn't merge. Conflicts against current main in source/mcp/mcp-client.ts, source/mcp/mcp-client.spec.ts, and docs/configuration/mcp-configuration.md. Please rebase.

2. Resource mentions bypass the file-mention size guard. The new RESOURCE case in source/utils/prompt-processor.ts inlines the whole body unconditionally. The FILE case right above it deliberately doesn't - over FILE_MENTION_INLINE_MAX_LINES it emits a head preview plus a read_file(...) hint, with the comment "so a single @-mention can't flood the conversation". One @-mention of a large MCP resource dumps everything into context. Same cap should apply.

3. The server name gets stamped twice. source/components/user-input.tsx passes fileCompletions[selectedFileIndex]?.displayPath as the resourceName argument, but displayPath is `${resource.name} (${resource.serverName})`. So the chip reads [@api-docs (docs-server)] and the assembled header becomes === MCP Resource: api-docs (docs-server) (from docs-server) ===. The unit test passes a bare 'resource.txt' there, so it doesn't catch this. Pass resource.name through instead of the display string.

4. Multi-message prompts collapse into one user turn. getPrompt preserves role on each message, then mcp-prompt-handler.ts joins every message's text with \n\n and sends it as a single onHandleChatMessage. A few-shot prompt with assistant turns - a normal MCP prompt shape - loses its structure.

5. Capability gating is claimed but not implemented. The commit message and changeset both say discovery is "gated on the server's declared capabilities", but connectToServer calls listResources() / listPrompts() unconditionally and swallows the failure. That's two extra round trips per server that supports neither, and the "MCP server does not support resources" log line is a guess. client.getServerCapabilities() is available post-connect - either use it or fix the claim.

Non-blocking

  • handleResourceMention returns null on any read failure, so a server error, a timeout, and a missing resource all look identical: the mention silently does nothing. Mirrors handleFileMention, but a remote read failing silently is a worse trade than a local file not existing.
  • Positional args beyond the prompt's declared arguments are dropped with no warning, as are all args when the prompt declares none.
  • Issue #1162's phase 2 called for prompts to go through source/commands/lazy-registry.ts; this intercepts in handleSlashCommand instead. Fine functionally (custom commands are correctly checked first), but worth a note on why.
  • No tests cover the user-input.tsx wiring. encodeMCPResourcePath / decodeMCPResourcePath and the merged completion list are the riskiest new code here and are only exercised manually.
  • getServerInfo now returns resourceCount / promptCount, but neither /mcp nor /doctor shows them.
  • docs/battlemap.md is the parity claim this closes, and it isn't updated.

Solid

Server-scoped readResource / getPrompt rather than cross-server URI search, with a test proving two same-named prompts route correctly. Text vs blob discrimination by key presence rather than a nonexistent type field - correct per the MCP schema and directly tested. Binary blocks become a note instead of raw base64. Discovery failures can't break tool discovery. disconnect() clears the new maps. Changeset names the right package.

The MCP client implemented exactly two operations, listTools and
callTool - no listResources, readResource, listPrompts, or getPrompt
anywhere in source/mcp/. docs/battlemap.md claims client parity with
Claude Code, which additionally surfaces MCP resources as @-mentions
and MCP prompts as slash commands.

MCPClient discovers a server's resources and prompts alongside its
tools at connect time, gated on the server's declared capabilities and
best-effort (a listResources/listPrompts failure logs and leaves that
server with zero of that kind, the same as a server that never
declared the capability - it must not fail tool discovery, which is
the connection's real contract). readResource and getPrompt take an
explicit serverName rather than searching every connected server by
URI/name, so two servers that happen to expose the same URI or prompt
name can never be confused with each other - the completion or command
that triggers either already knows which server it came from.

Resources join the file-mention system: `@` fuzzy-searches local
filenames and connected servers' resources together (same completion
list, same Tab-to-select, distinguished by an encoded path prefix so
selection knows which reader to call), and a selected resource is read
and inlined as a placeholder exactly the way a file mention already
works. A binary content block becomes a short placeholder note instead
of its raw base64 landing in the prompt.

Prompts dispatch as `/mcp:<server>:<prompt>`, listed alongside custom
commands in the `/` completion menu. Positional arguments fill the
prompt's declared parameters in order (not all collapsing onto the
first, and not silently swallowed past the first missing one - every
missing required argument is named before anything is sent). Unlike a
custom command's static template, the prompt is fetched fresh from its
server on every invocation and the result is sent as the next chat
turn via the same onHandleChatMessage path a custom command uses -
not merely displayed, which is what makes it usable as a command
rather than a lookup.

Sampling, elicitation, and roots are out of scope for this change (the
proposal itself calls for staging them separately, since they require
the client to act as a server back toward the MCP host).

Refs Nano-Collective#1162.
Blocking:
- Gate resource/prompt discovery on the server's declared capabilities
  (getServerCapabilities()) instead of attempting listResources/listPrompts
  unconditionally and swallowing the failure.
- Apply the same FILE_MENTION_INLINE_MAX_LINES guard to MCP resource
  mentions so a single large @-mention can't flood the conversation.
- Stop double-stamping the server name on a resource mention chip - pass
  the bare resource name instead of the completion list's disambiguated
  displayPath.
- Preserve per-message roles when a prompt returns multiple messages
  (e.g. a few-shot example) instead of flattening them into one user
  turn. handleChatMessage now accepts optional historyMessages spliced
  in ahead of the new user message.

Non-blocking:
- Surface a failed resource read via logError instead of failing
  silently, unlike a missing local file which the user can already see.
- Warn instead of silently dropping positional args beyond what a
  prompt declares (including all args when it declares none).
- Note why MCP prompts intercept in handleSlashCommand rather than
  going through the static lazy-registry.
- Cover the user-input.tsx resource-mention wiring with a test.
- Show resource/prompt counts and names in /mcp and /doctor.
- Update docs/battlemap.md's MCP parity claim to call out resources
  and prompts specifically.

Also rebases onto current main to resolve the branch's merge conflicts.
@addyCooks
addyCooks force-pushed the feat/mcp-resources-prompts branch from c57d330 to 049c2bb Compare September 7, 2026 21:39
@addyCooks

Copy link
Copy Markdown
Contributor Author

Thanks @will-lamerton,
for the thorough review,

here's everything addressed:

Blocking

  1. Rebased onto main; conflicts resolved.
  2. Resource mentions now share the same FILE_MENTION_INLINE_MAX_LINES guard as files,
    large resources get a head preview instead of being dumped in full.
  3. Fixed passes resource.name instead of the display string, so the chip and header no longer double-stamp the server name.
  4. getPrompt's per-message roles are now preserved: earlier turns splice into history as-is, and only the final user turn triggers the chat round-trip.
  5. Discovery is now actually gated on client.getServerCapabilities() listResources/listPrompts only fire when the server declares that capability.

Non-blocking

  • A failed resource read now surfaces via logError instead of failing silently.
  • Extra/unexpected positional args now warn instead of vanishing.
  • Added a comment explaining why prompts intercept in handleSlashCommand instead of lazy-registry.ts.
  • Added test coverage for the user-input.tsx wiring (encode/decode + selection flow).
  • /mcp and /doctor now show resource/prompt counts and names.
  • Updated docs/battlemap.md's parity claim to call out resources/prompts specifically.

Resolves the user-input.tsx conflict. main rewrote the component's render
layer (new-ui: TitledBoxWithPreferences, promptWidth, the `?` shortcuts
overlay, removal of the terminal-mouse selection-mode block), so main's
version is taken wholesale and this branch's MCP-resource additions are
re-applied on top:

- getToolManager / fuzzyScoreFilePath / handleResourceMention imports
- MCP_RESOURCE_PATH_PREFIX with encode/decode helpers
- getMCPResourceCompletions, merged into the `@` completion list
- fileCompletions state widened with displayPath / resourceName
- mcp:<server>:<prompt> names in the slash-command completions
- the resource-vs-file branch in handleFileSelection
- displayPath rendering for resource rows in the suggestions list
will-lamerton
will-lamerton previously approved these changes Sep 15, 2026

@will-lamerton will-lamerton left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebase done — I merged current main in and resolved the user-input.tsx conflict myself (2e4991a). main had rewritten that component's render layer, so I took main's version wholesale and re-applied the seven MCP-resource additions on top; the branch now differs from main by exactly the 25 files and 2265/52 lines this PR intended, with main a clean ancestor.

Re-verified all five blocking items in the code rather than taking the reply at face value — the size guard, the bare resource.name, the capability gating via getServerCapabilities(), and the historyMessages splice are all genuinely there, and the non-user-terminal prompt case you handled on top of #4 is a nice catch I hadn't asked for. All seven non-blocking items addressed too.

Local on the merged head: tsc clean, biome clean, knip clean, 292 tests pass across the touched specs plus the user-input and app-util suites. Thanks for the thorough turnaround.

Unrelated to this PR's feature work, but main is red on
`pnpm run test:format` and that blocks this branch from merging.

Nano-Collective#1279 (126e76f) added the path-validators import and Nano-Collective#1289 (0648d75)
added the inline-diff import. Each branch was internally sorted and
green on its own; merge commit 9331255 interleaved the two lists in
the wrong order, and no single PR's CI ever saw the combination.

Pure import reorder from `biome check --write`, no behavior change.
@github-actions github-actions Bot added the area:tools Tool implementations and tool-calling label Sep 15, 2026
@will-lamerton
will-lamerton merged commit 1a570b7 into Nano-Collective:main Sep 15, 2026
16 checks passed
@addyCooks

Copy link
Copy Markdown
Contributor Author

Thanks @will-lamerton,
and thanks for handling the user-input.tsx conflict yourself after the render-layer rewrite,
and for sorting the write-file.tsx imports.
Glad the non-user-terminal prompt case was useful.

Since this covered phases 1 and 2 of #1162,
I'm happy to pick up phase 3 (sampling / elicitation / roots) as a separate PR if that's wanted.
Otherwise I'll open a tracking issue for it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:docs Documentation area:tools Tool implementations and tool-calling area:tui Terminal UI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] MCP resources, prompts, and sampling support

2 participants