From bbaa84720ca588e5433c2bd25dc6f9cbce5dc6b9 Mon Sep 17 00:00:00 2001 From: John Lancaster <32917998+jsl12@users.noreply.github.com> Date: Sun, 30 Aug 2026 11:42:26 -0500 Subject: [PATCH] prompts and resources as tools --- src/personal_mcp/docs/copilot.md | 29 ++++++++++++++++++++++++----- src/personal_mcp/docs/mcp_layout.md | 10 +++++++--- src/personal_mcp/mcp.py | 11 +++++++++-- tests/server_contract.py | 14 +++++++++++++- 4 files changed, 53 insertions(+), 11 deletions(-) diff --git a/src/personal_mcp/docs/copilot.md b/src/personal_mcp/docs/copilot.md index c8ac409..98a8c29 100644 --- a/src/personal_mcp/docs/copilot.md +++ b/src/personal_mcp/docs/copilot.md @@ -16,7 +16,7 @@ Copilot interacts with MCP servers through independently exposed lanes: 2. resources attached as read-only context 3. server-provided prompts -This server publishes skills as native `skill://` resources, general docs as `resource://docs/{path*}` resources, and workflows as native MCP prompt objects. It intentionally publishes no compatibility tools that mirror resources or prompts. +This server publishes skills as native `skill://` resources, general docs as `resource://docs/{path*}` resources, and workflows as native MCP prompt objects. It also publishes four compatibility tools generated by FastMCP's resource and prompt transforms, so tool-only chat surfaces can reach the same content without relying on resource or prompt UI support. ## VS Code Feature Coverage @@ -25,12 +25,12 @@ The server uses every FastMCP feature that applies to its read-only guidance wor | Feature | Usage | | --- | --- | | Server identity | The initialize response includes a stable name, usage instructions, and a self-contained icon for VS Code's MCP server UI. | -| Tools | No tools are published for this documentation-only server surface. Resource and prompt operations stay on native MCP capabilities. | +| Tools | `list_resources` and `read_resource` expose skills and docs; `list_prompts` and `get_prompt` expose prompt discovery and rendering. All four are generated by FastMCP transforms and delegate to the native server surfaces. | | Resources | Documentation and skills use native resources and wildcard resource templates with explicit Markdown MIME types. | -| Prompts | Declarative workflows use native prompt objects with descriptions, display titles, typed arguments, and slash-command access. | +| Prompts | Declarative workflows use native prompt objects with descriptions, display titles, typed arguments, and explicit `/.` invocation in VS Code chat. | | Argument completion | Prompt arguments with authored `choices` are returned through `completion/complete` as the user types. | -[FastMCP server identity](https://gofastmcp.com/servers/server), [component icons](https://gofastmcp.com/servers/icons), [tool metadata](https://gofastmcp.com/servers/tools), and [argument completion](https://gofastmcp.com/servers/completions) define the implementation details. [VS Code's MCP documentation](https://code.visualstudio.com/docs/agent-customization/mcp-servers) describes how tools, resources, prompts, and MCP Apps appear in the client. +[FastMCP server identity](https://gofastmcp.com/servers/server), [resource tools](https://gofastmcp.com/servers/transforms/resources-as-tools), [prompt tools](https://gofastmcp.com/servers/transforms/prompts-as-tools), and [argument completion](https://gofastmcp.com/servers/completions) define the implementation details. [VS Code's MCP documentation](https://code.visualstudio.com/docs/agent-customization/mcp-servers) describes how tools, resources, prompts, and MCP Apps appear in the client. The following capabilities are conditional rather than useful by default: @@ -73,6 +73,21 @@ For autonomous agents: For manual context attachment, browse the server's resources and attach the same bounded set of files. +## Tool Fallbacks + +When a chat surface cannot browse or attach MCP resources: + +1. Call `list_resources` to see every skill and docs resource, including manifests and supporting-file templates. +2. Call `read_resource` with the exact `uri` (or a filled-in template such as `skill:///`) to fetch content. + +When an agent needs to discover or render prompts without explicit slash-command invocation: + +1. Call `list_prompts` to get each prompt's name, description, and required or optional arguments. +2. Call `get_prompt` with the prompt `name` and an optional `arguments` object. +3. Treat the returned JSON `messages` array as the rendered workflow context. + +These tools route through the same native resource and prompt surfaces, so provider validation, middleware, visibility, and live prompt discovery still apply. + ## Prompt Examples Resource attachment: @@ -111,7 +126,11 @@ Instructions steer behavior but do not force VS Code to attach resources automat ## Prompt Objects -Prompts remain separate from skills. When the client supports MCP prompt APIs, use prompt listing and `get_prompt` for parameterized workflows. Each authored `PROMPT.md` is the complete source of truth for its metadata, arguments, and prose; changes are loaded on the next prompt request. +Prompts remain separate from skills. VS Code presents native MCP prompts as explicit `/.` commands that users select in chat; this is the preferred path for a user intentionally starting a known parameterized workflow. Native prompt argument completion uses the authored choices where available. + +The compatibility tools cover the autonomous path: an agent can call `list_prompts`, select a workflow by its description, and call `get_prompt` with matching arguments. `get_prompt` returns the same rendered prompt as JSON containing its ordered MCP messages rather than starting a second chat request itself. + +Each authored `PROMPT.md` is the complete source of truth for metadata, arguments, and prose. Both native prompt requests and compatibility-tool calls resolve through the same provider, so changes are loaded on the next request. ## Troubleshooting diff --git a/src/personal_mcp/docs/mcp_layout.md b/src/personal_mcp/docs/mcp_layout.md index 3b70112..4f62eb1 100644 --- a/src/personal_mcp/docs/mcp_layout.md +++ b/src/personal_mcp/docs/mcp_layout.md @@ -52,6 +52,10 @@ flowchart TD B --> G[FastMCP] D --> G F --> G + G --> I[ResourcesAsTools] + G --> J[PromptsAsTools] + I --> N[list_resources / read_resource] + J --> O[list_prompts / get_prompt] G --> H[MCP Transport] H --> K[FastAPI Application] L[Pre-built site] --> M[Static /docs Mount] @@ -81,12 +85,12 @@ No runtime Markdown-to-HTML conversion occurs. 2. Skill supporting files map to `skill:///`. 3. Declarative prompt documents map to native MCP prompt names. 4. General `src/personal_mcp/docs/.md` maps to `resource://docs/{path*}`. - -The server publishes no tool projections of resources or prompts. +5. FastMCP's `ResourcesAsTools` transform projects every registered resource and template onto two compatibility tools, `list_resources` and `read_resource`, for tool-only clients. +6. FastMCP's `PromptsAsTools` transform projects every registered prompt onto `list_prompts` and `get_prompt`; rendering still routes through the native prompt provider. ## Public Surface Policy -Canonical provider and protocol surfaces are the only public interfaces. +Native resources and prompts remain the source of truth. Generated compatibility tools delegate to those surfaces instead of maintaining parallel catalogs or rendering logic. ## Static Mount Expectations diff --git a/src/personal_mcp/mcp.py b/src/personal_mcp/mcp.py index d736589..e1362f7 100644 --- a/src/personal_mcp/mcp.py +++ b/src/personal_mcp/mcp.py @@ -1,6 +1,8 @@ from __future__ import annotations from fastmcp import FastMCP +from fastmcp.server.transforms import PromptsAsTools +from fastmcp.server.transforms import ResourcesAsTools from mcp_types import Icon from .prompts.provider import prompt_lifespan @@ -10,8 +12,9 @@ from .skills import skill_lifespan _SERVER_INSTRUCTIONS = """Personal development guidance exposed as native MCP resources and prompts. -Use prompts for parameterized workflows. For task-specific guidance, browse native skill resources, -select one skill:///SKILL.md resource, and read its manifest only when supporting detail is needed. +Use prompts for parameterized workflows; tool-only agents can discover and render them with +list_prompts and get_prompt. For task-specific guidance, browse native skill resources, select one +skill:///SKILL.md resource, and read its manifest only when supporting detail is needed. """ _SERVER_ICON = Icon( src=( @@ -59,6 +62,10 @@ def create_mcp() -> FastMCP: def docs_markdown(path: str) -> dict[str, str]: return read_docs_markdown_path(registry, path) + # Bridges tool-only clients that cannot browse native resources or prompts directly. + mcp.add_transform(ResourcesAsTools(mcp)) + mcp.add_transform(PromptsAsTools(mcp)) + return mcp diff --git a/tests/server_contract.py b/tests/server_contract.py index 57377a2..6c6db59 100644 --- a/tests/server_contract.py +++ b/tests/server_contract.py @@ -1,4 +1,5 @@ from fastmcp import Client +from mcp_types import TextContent from mcp_types import TextResourceContents @@ -6,8 +7,9 @@ async def assert_server_contract(client: Client) -> None: resources = {str(resource.uri) for resource in await client.list_resources()} templates = {str(template.uri_template) for template in await client.list_resource_templates()} prompts = {prompt.name for prompt in await client.list_prompts()} + tools = {tool.name for tool in await client.list_tools()} - assert await client.list_tools() == [] + assert tools == {"get_prompt", "list_prompts", "list_resources", "read_resource"} assert "skill://pytesting/SKILL.md" in resources assert "resource://docs/{path*}" in templates assert "skill://pytesting/{path*}" in templates @@ -20,3 +22,13 @@ async def assert_server_contract(client: Client) -> None: assert "# Pytesting" in skill_content[0].text assert isinstance(docs_content[0], TextResourceContents) assert '"format": "markdown"' in docs_content[0].text + + listed = await client.call_tool("list_resources") + listed_content = listed.content[0] + assert isinstance(listed_content, TextContent) + assert "skill://pytesting/SKILL.md" in listed_content.text + + read = await client.call_tool("read_resource", {"uri": "skill://pytesting/SKILL.md"}) + read_content = read.content[0] + assert isinstance(read_content, TextContent) + assert "# Pytesting" in read_content.text