diff --git a/Dockerfile b/Dockerfile index a38ebe0..1e483d0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,6 +29,9 @@ FROM python:3.14-slim AS runtime ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ PATH="/app/.venv/bin:$PATH" \ + PERSONAL_MCP_HOST=0.0.0.0 \ + PERSONAL_MCP_PORT=8765 \ + PERSONAL_MCP_RELOAD=false \ PERSONAL_MCP_SITE_DIR=/app/site WORKDIR /app @@ -51,4 +54,4 @@ RUN --mount=type=cache,target=/root/.cache/uv \ USER appuser -CMD ["uvicorn", "personal_mcp.main:create_app", "--factory", "--host", "0.0.0.0", "--port", "8765"] +ENTRYPOINT ["python", "-m", "personal_mcp"] \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index d4ebc86..6e6006d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ version = "2.0.0" requires-python = ">=3.12" dependencies = [ "fastapi>=0.133.0", - "fastmcp==4.0.0b1", + "fastmcp==4.0.0b4", "pydantic-settings>=2", "pyyaml>=6.0.2", "python-json-logger>=4", @@ -13,7 +13,7 @@ dependencies = [ ] [tool.uv] -constraint-dependencies = ["fastmcp-slim==4.0.0b1"] +constraint-dependencies = ["fastmcp-slim==4.0.0b4"] [project.scripts] personal-mcp = "personal_mcp.__main__:main" diff --git a/src/personal_mcp/docs/contracts/uris.md b/src/personal_mcp/docs/contracts/uris.md index fcb69b1..85e219d 100644 --- a/src/personal_mcp/docs/contracts/uris.md +++ b/src/personal_mcp/docs/contracts/uris.md @@ -60,9 +60,7 @@ For skills: 4. read `_manifest` when supporting material may be needed 5. fetch only the supporting paths relevant to the task -Tool-only agents may call `search_skills` instead of retrieving the complete resource list. Search results contain only provider-derived names, descriptions, and canonical main-resource URIs; skill content remains available exclusively through the native resource contract. - -For prompts, use the native MCP prompt APIs or their generic tool projection. +For prompts, use the native MCP prompt APIs. ## Stability Policy diff --git a/src/personal_mcp/docs/copilot.md b/src/personal_mcp/docs/copilot.md index 1e42479..c8ac409 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 and prompts as native MCP prompt objects. It also exposes `search_skills`, `list_resources`, and `read_resource` tools for agents whose tool catalog does not include direct MCP resource operations. +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. ## VS Code Feature Coverage @@ -25,7 +25,7 @@ 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 | Compatibility tools have display titles, structured output schemas, and read-only, idempotent, closed-world annotations. FastMCP's default schema dereferencing remains enabled for clients such as VS Code that require flat schemas. | +| Tools | No tools are published for this documentation-only server surface. Resource and prompt operations stay on native MCP capabilities. | | 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. | | Argument completion | Prompt arguments with authored `choices` are returned through `completion/complete` as the user types. | @@ -66,11 +66,10 @@ A successful `resources/list` response does not guarantee the picker appears in For autonomous agents: -1. call `search_skills` with the task, capability, or technology -2. compare the bounded main-skill matches -3. call `read_resource` for one relevant `skill:///SKILL.md` -4. read `_manifest` only if supporting detail may be needed -5. read only selected supporting files +1. browse native resources and compare skill descriptions +2. read one relevant `skill:///SKILL.md` +3. read `_manifest` only if supporting detail may be needed +4. read only selected supporting files For manual context attachment, browse the server's resources and attach the same bounded set of files. @@ -102,13 +101,13 @@ A repo-level instruction should name the native retrieval order and context budg When a task matches a personal-mcp skill: 1. Prefer an already attached native skill resource. -2. Otherwise call `search_skills` and select one `skill:///SKILL.md` result by description. -3. Call `read_resource` for the selected skill and read `_manifest` only when supporting material is needed. +2. Otherwise browse MCP resources and select one `skill:///SKILL.md` by description. +3. Read the selected skill and read `_manifest` only when supporting material is needed. 4. Load at most two candidate main files and only the relevant supporting paths. 5. Reconcile guidance with the current repository before editing. ``` -Instructions steer behavior but do not force VS Code to attach resources automatically. The generic tools provide an agent-callable fallback when direct resource operations are absent from the deferred-tool catalog. +Instructions steer behavior but do not force VS Code to attach resources automatically. ## Prompt Objects @@ -118,7 +117,7 @@ Prompts remain separate from skills. When the client supports MCP prompt APIs, u 1. Use `MCP: List Servers` to confirm the server is enabled. 2. Use `MCP: Browse Resources` to confirm native skill resources exist. -3. Confirm `search_skills` and `read_resource` appear in the chat tool picker when autonomous retrieval is required. +3. Confirm `Add Context > MCP Resources` lists server resources in the active chat surface. 4. Restart the MCP server after changing skill files because production uses `reload=False`. 5. Reload the VS Code window if the server is healthy but the resource or tool picker remains stale. diff --git a/src/personal_mcp/docs/skills/mcp-details/references/sdk-and-fastmcp.md b/src/personal_mcp/docs/skills/mcp-details/references/sdk-and-fastmcp.md index 987826e..32ca8ae 100644 --- a/src/personal_mcp/docs/skills/mcp-details/references/sdk-and-fastmcp.md +++ b/src/personal_mcp/docs/skills/mcp-details/references/sdk-and-fastmcp.md @@ -14,14 +14,18 @@ Use this page for implementation-oriented links across MCP SDKs and FastMCP. !!! info "FastMCP sources" - [FastMCP project documentation](https://gofastmcp.com/) - [FastMCP server identity and behavior](https://gofastmcp.com/servers/server) + - [FastMCP providers overview](https://gofastmcp.com/servers/providers/overview) + - [FastMCP custom providers](https://gofastmcp.com/servers/providers/custom) + - [FastMCP skills provider](https://gofastmcp.com/servers/providers/skills) - [FastMCP tools and annotations](https://gofastmcp.com/servers/tools) - [FastMCP resources and templates](https://gofastmcp.com/servers/resources) - [FastMCP prompts](https://gofastmcp.com/servers/prompts) + - [FastMCP filesystem provider](https://gofastmcp.com/servers/providers/filesystem) - [FastMCP argument completion](https://gofastmcp.com/servers/completions) - [FastMCP component icons](https://gofastmcp.com/servers/icons) - [FastMCP Apps](https://gofastmcp.com/apps/overview) - - [FastMCP GitHub repository](https://github.com/jlowin/fastmcp) - - [FastMCP examples directory](https://github.com/jlowin/fastmcp/tree/main/examples) + - [FastMCP GitHub repository](https://github.com/PrefectHQ/fastmcp) + - [FastMCP examples directory](https://github.com/PrefectHQ/fastmcp/tree/main/examples) - [FastMCP PyPI package](https://pypi.org/project/fastmcp/) ## Server Implementation Patterns diff --git a/src/personal_mcp/docs/usage.md b/src/personal_mcp/docs/usage.md index 3e3cca6..30ef42d 100644 --- a/src/personal_mcp/docs/usage.md +++ b/src/personal_mcp/docs/usage.md @@ -53,15 +53,15 @@ These utilities operate directly on the native `skill://` contract and require n In VS Code, skills can arrive through: 1. explicit attachment from `Add Context > MCP Resources` or `MCP: Browse Resources` -2. the `search_skills` and `read_resource` tools for autonomous agents +2. direct resource reads on selected `skill:///SKILL.md` entries 3. a slash-command prompt that names a specific native skill URI -Instructions can steer retrieval, but they do not guarantee automatic resource attachment in every chat surface. When the deferred-tool catalog omits direct MCP resource operations, use the tools; search derives matches from native provider metadata and `read_resource` delegates to the provider without duplicating skill content. +Instructions can steer retrieval, but they do not guarantee automatic resource attachment in every chat surface. Use `MCP: Browse Resources` to confirm server-side availability, then attach only the minimum skill resources needed for the current task. A reliable prompt is: ```text -Call search_skills with the task or capability and select the best matching skill://.../SKILL.md result. Use read_resource for one selected skill, inspect its _manifest only if supporting detail is needed, and reconcile the guidance with this workspace. +Browse MCP resources, select the best matching skill://.../SKILL.md entry by description, inspect its _manifest only if supporting detail is needed, and reconcile the guidance with this workspace. ``` ## Thin Shim Pattern diff --git a/src/personal_mcp/mcp.py b/src/personal_mcp/mcp.py index b54c40a..d1b76c6 100644 --- a/src/personal_mcp/mcp.py +++ b/src/personal_mcp/mcp.py @@ -1,15 +1,10 @@ from __future__ import annotations -import re -from typing import Annotated - from fastmcp import FastMCP -from mcp.types import CompletionArgument -from mcp.types import CompletionContext -from mcp.types import Icon -from mcp.types import PromptReference -from pydantic import BaseModel -from pydantic import Field +from mcp_types import CompletionArgument +from mcp_types import CompletionContext +from mcp_types import Icon +from mcp_types import PromptReference from personal_mcp.prompts import create_prompts_provider from personal_mcp.prompts.models import MarkdownPrompt @@ -21,9 +16,8 @@ from personal_mcp.skills import create_skills_provider _SERVER_INSTRUCTIONS = """Personal development guidance exposed as native MCP resources and prompts. -Use prompts for parameterized workflows. For task-specific guidance, search skills, select one -skill:///SKILL.md result, and read its manifest only when supporting detail is needed. Read-only -compatibility tools expose the same resource catalog to clients without native resource access. +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. """ _SERVER_ICON = Icon( src=( @@ -47,51 +41,6 @@ def _ro_annotations() -> dict[str, bool]: } -class SkillSearchResult(BaseModel): - """Metadata needed to select a native skill resource.""" - - name: str - description: str - uri: str - - -class SkillSearchResponse(BaseModel): - """Bounded skill matches for an agent search query.""" - - results: list[SkillSearchResult] - - -def _skill_search_score(name: str, description: str, query: str) -> int: - normalized_name = name.casefold().replace("-", " ") - normalized_description = description.casefold() - normalized_query = " ".join(query.casefold().split()) - terms = tuple(dict.fromkeys(re.findall(r"[a-z0-9]+", normalized_query))) - if not terms: - return 0 - - name_terms = set(normalized_name.split()) - description_terms = set(re.findall(r"[a-z0-9]+", normalized_description)) - score = 100 if normalized_query == normalized_name else 0 - matched_terms = 0 - for term in terms: - term_score = 0 - if term in name_terms: - term_score = 20 - elif term in normalized_name: - term_score = 10 - elif term in description_terms: - term_score = 4 - elif term in normalized_description: - term_score = 1 - if term_score: - matched_terms += 1 - score += term_score - - if matched_terms == len(terms): - score += 10 - return score - - def _register_components(mcp: FastMCP, registry: DocsRegistry) -> None: @mcp.resource( "resource://docs/{path*}", @@ -106,95 +55,6 @@ def _register_components(mcp: FastMCP, registry: DocsRegistry) -> None: return read_docs_markdown_path(registry, path) -def _register_resource_tools(mcp: FastMCP) -> None: - @mcp.tool( - name="search_skills", - title="Search Skills", - description=( - "Find task-specific skill guidance by capability, technology, problem, or workflow. " - "Returns skill metadata and canonical URIs only; use read_resource to load a selected result." - ), - tags={"search", "skills"}, - annotations=_ro_annotations(), - ) - async def search_skills( - query: Annotated[ - str, - Field( - min_length=2, - max_length=300, - description="Capability, technology, problem, or workflow to find.", - ), - ], - limit: Annotated[ - int, - Field(ge=1, le=10, description="Maximum number of matches to return."), - ] = 5, - ) -> SkillSearchResponse: - resources = await mcp.list_resources() - matches: list[tuple[int, SkillSearchResult]] = [] - for resource in resources: - uri = str(resource.uri) - if not uri.startswith("skill://") or not uri.endswith("/SKILL.md"): - continue - - name = uri.removeprefix("skill://").removesuffix("/SKILL.md") - description = resource.description or "" - score = _skill_search_score(name, description, query) - if score: - matches.append( - ( - score, - SkillSearchResult(name=name, description=description, uri=uri), - ) - ) - - matches.sort(key=lambda match: (-match[0], match[1].name)) - return SkillSearchResponse(results=[match[1] for match in matches[:limit]]) - - @mcp.tool( - name="list_resources", - title="List Resources", - description=( - "List available MCP resources and URI templates. Use before read_resource to discover skill guidance." - ), - annotations=_ro_annotations(), - ) - async def list_resources() -> dict[str, list[dict[str, str | None]]]: - resources = await mcp.list_resources() - templates = await mcp.list_resource_templates() - return { - "resources": [ - { - "uri": str(resource.uri), - "name": resource.name, - "description": resource.description, - "mime_type": resource.mime_type, - } - for resource in resources - ], - "templates": [ - { - "uri_template": template.uri_template, - "name": template.name, - "description": template.description, - "mime_type": template.mime_type, - } - for template in templates - ], - } - - @mcp.tool( - name="read_resource", - title="Read Resource", - description="Read a resource URI returned by list_resources, including skill files, manifests, and references.", - annotations=_ro_annotations(), - ) - async def read_resource(uri: str) -> dict[str, object]: - result = await mcp.read_resource(uri) - return result.model_dump(mode="json", exclude_none=True) - - def _register_prompt_completions(mcp: FastMCP, provider: MarkdownPromptsProvider) -> None: @mcp.completion async def complete_prompt_argument( @@ -230,6 +90,5 @@ def create_mcp() -> FastMCP: prompts_provider = create_prompts_provider() mcp.add_provider(prompts_provider) mcp.add_provider(create_skills_provider()) - _register_resource_tools(mcp) _register_prompt_completions(mcp, prompts_provider) return mcp diff --git a/uv.lock b/uv.lock index 1973662..6030cc1 100644 --- a/uv.lock +++ b/uv.lock @@ -7,7 +7,7 @@ resolution-markers = [ ] [manifest] -constraints = [{ name = "fastmcp-slim", specifier = "==4.0.0b1" }] +constraints = [{ name = "fastmcp-slim", specifier = "==4.0.0b4" }] [[package]] name = "aiofile" @@ -507,19 +507,19 @@ wheels = [ [[package]] name = "fastmcp" -version = "4.0.0b1" +version = "4.0.0b4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastmcp-slim", extra = ["client", "server"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/fd/e513c524bb3e296203f65bd8e9e726e9236bd3b59315e7cbdeb095662c01/fastmcp-4.0.0b1.tar.gz", hash = "sha256:f98d69588a73e1672840558641d5d0f111e207baffe001f3465713a53ebb6b4c", size = 42065171, upload-time = "2026-07-28T21:18:15.312Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/a1/032858e40579c3047ac6f3eb1e97b59db7ad649234d55094a9bcf2d560c1/fastmcp-4.0.0b4.tar.gz", hash = "sha256:e3a8b0f4a8300b3da15584e7bff62d464fa5458b52903d86a6a2e149c61c3edb", size = 42180304, upload-time = "2026-08-26T22:59:07.827Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/66/41b503ef852eff83f3f0c04f46cd1fc738c5374fd509384fc6b96e45918f/fastmcp-4.0.0b1-py3-none-any.whl", hash = "sha256:d66eb7b0763ffff2ae0fc573778ea25604dcb7e59769e5afaf9851a806eb1129", size = 8064, upload-time = "2026-07-28T21:18:12.688Z" }, + { url = "https://files.pythonhosted.org/packages/65/11/132d9030c8b3235784352e7132286bf2e4d010f4f295ba4b4ccf2c44ebd1/fastmcp-4.0.0b4-py3-none-any.whl", hash = "sha256:2996abab765172bfcf2e7d4bdde77438390ecd79af5b5a3af586e97eedf8f5e4", size = 8088, upload-time = "2026-08-26T22:59:04.265Z" }, ] [[package]] name = "fastmcp-slim" -version = "4.0.0b1" +version = "4.0.0b4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mcp-types" }, @@ -530,9 +530,9 @@ dependencies = [ { name = "rich" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bc/33/f207166aac88c6d8be1b2a82c54fecd1e04b075ef12d027aa17b3134fc0f/fastmcp_slim-4.0.0b1.tar.gz", hash = "sha256:158efb25720e0cb301711146b2d05d181de95cd91fe70e87c251ad14b123d665", size = 660081, upload-time = "2026-07-28T21:17:50.171Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/1a/89f4ade73ba26c04b573464686a2bc656b1f910d3f92f6e0950971a7c64f/fastmcp_slim-4.0.0b4.tar.gz", hash = "sha256:9a57dfa7be461d6a65903f450af6e6b51d26266f50977bf7c30508fefaab8816", size = 679556, upload-time = "2026-08-26T22:58:39.69Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/5d/fbe192d2ab50bb31b284fd0eb6c72d4347df7a57d836bee4f1973ffd1d7d/fastmcp_slim-4.0.0b1-py3-none-any.whl", hash = "sha256:dd907a3db5a2f479ca958c30157e227b4f7b340a56d333eb91de95719254597a", size = 827975, upload-time = "2026-07-28T21:17:48.747Z" }, + { url = "https://files.pythonhosted.org/packages/15/7c/abf53a150e379563efc353e510ef412aae977929e7b289e1cd089002ffd3/fastmcp_slim-4.0.0b4-py3-none-any.whl", hash = "sha256:d20afd384efe388302d42280bdd2f5525af57d317e315f8e065a8de7517302b5", size = 852891, upload-time = "2026-08-26T22:58:38.348Z" }, ] [package.optional-dependencies] @@ -1194,7 +1194,7 @@ test = [ [package.metadata] requires-dist = [ { name = "fastapi", specifier = ">=0.133.0" }, - { name = "fastmcp", specifier = "==4.0.0b1" }, + { name = "fastmcp", specifier = "==4.0.0b4" }, { name = "pydantic-settings", specifier = ">=2" }, { name = "python-json-logger", specifier = ">=4" }, { name = "pyyaml", specifier = ">=6.0.2" }, @@ -1932,11 +1932,11 @@ wheels = [ [[package]] name = "uncalled-for" -version = "0.3.2" +version = "0.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/82/345cc927f7fbdae6065e7768759932fcc827fc20b29b45dfbafa2f1f7da4/uncalled_for-0.3.2.tar.gz", hash = "sha256:89f5dbcd71e2b8f47c030b1fa302e6cce2ec795d1ac565eeb6525c5fe55cb8a2", size = 50032, upload-time = "2026-05-06T13:38:25.204Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5a/92ce0b3ea5481915f55da994c2c2c5f7a3c09949afde196ee89f8ab961aa/uncalled_for-0.4.0.tar.gz", hash = "sha256:335b95bd2422332ec210d518f314a16e4c640921c39fc8bf2ad095bd3538f4af", size = 56979, upload-time = "2026-08-10T14:51:46.247Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/25/2c87754f3a9e692315f7b811244090e68f362979fc8886b3fbd2985a1d8c/uncalled_for-0.3.2-py3-none-any.whl", hash = "sha256:0ff60b142c7d1f8070bde9d42afaa70aedc77dcc10998c227687e9c15713418e", size = 11444, upload-time = "2026-05-06T13:38:24.025Z" }, + { url = "https://files.pythonhosted.org/packages/a2/40/97cec87c077eb3291fc7905e6633e08b7ca593c57d30238444bcb6bb3d53/uncalled_for-0.4.0-py3-none-any.whl", hash = "sha256:16c4bb3337532e4bd5569adc192285976f3ad5305402256d34c67a12b5c968bd", size = 15502, upload-time = "2026-08-10T14:51:45.068Z" }, ] [[package]]