Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ec12a100a | ||
|
|
9be7c27410 | ||
|
|
a157489634 |
+1
-1
@@ -16,7 +16,7 @@ dependencies = [
|
||||
constraint-dependencies = ["fastmcp-slim==4.0.0b1"]
|
||||
|
||||
[project.scripts]
|
||||
personal-mcp = "personal_mcp.main:main"
|
||||
personal-mcp = "personal_mcp.__main__:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
|
||||
@@ -60,6 +60,8 @@ 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.
|
||||
|
||||
## Stability Policy
|
||||
|
||||
@@ -16,7 +16,32 @@ 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.
|
||||
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.
|
||||
|
||||
## VS Code Feature Coverage
|
||||
|
||||
The server uses every FastMCP feature that applies to its read-only guidance workload:
|
||||
|
||||
| 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. |
|
||||
| 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. |
|
||||
|
||||
[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.
|
||||
|
||||
The following capabilities are conditional rather than useful by default:
|
||||
|
||||
1. MCP Apps require an interactive tool result such as a form or visualization; this server returns guidance and structured resource data only.
|
||||
2. Sampling is appropriate only when server-side work must ask VS Code to run an LLM. The current server retrieves authored content and does not generate it.
|
||||
3. Elicitation is appropriate only when a running operation needs additional user input. Prompt arguments already collect all required input before execution.
|
||||
4. Progress, client logging, and background tasks require long-running operations. Current reads and prompt rendering are bounded local operations.
|
||||
5. Client roots matter only when server behavior depends on client filesystem roots. This server reads packaged content and never traverses a client workspace.
|
||||
6. `website_url` requires a canonical public deployment URL. None is configured, so the server does not advertise a guessed address.
|
||||
|
||||
Add one of these capabilities when a concrete workflow needs it, then cover its negotiated capability and protocol response in the HTTP MCP smoke tests. See the [FastMCP Apps overview](https://gofastmcp.com/apps/overview), [sampling](https://gofastmcp.com/servers/sampling), [elicitation](https://gofastmcp.com/servers/elicitation), [progress reporting](https://gofastmcp.com/servers/progress), and [MCP context](https://gofastmcp.com/servers/context) for the activation criteria.
|
||||
|
||||
## Native Skill Resources
|
||||
|
||||
@@ -26,7 +51,7 @@ For every skill, Copilot can discover:
|
||||
2. `skill://<name>/_manifest`
|
||||
3. `skill://<name>/{path*}` supporting-file template
|
||||
|
||||
The main resource description comes from `SKILL.md`. The manifest discloses supporting paths, sizes, and SHA256 hashes. This is the only skill discovery contract; there is no parallel skill catalog.
|
||||
The main resource description comes from `SKILL.md`. The manifest discloses supporting paths, sizes, and SHA256 hashes. Native resources remain the only skill content and discovery contract; the tools search or delegate to that same resource surface rather than maintaining a parallel catalog.
|
||||
|
||||
## Resource Picker Availability
|
||||
|
||||
@@ -39,10 +64,15 @@ A successful `resources/list` response does not guarantee the picker appears in
|
||||
|
||||
## Recommended Workflow
|
||||
|
||||
1. browse the server's resources
|
||||
2. attach one relevant `skill://<name>/SKILL.md`
|
||||
3. attach `_manifest` only if supporting detail may be needed
|
||||
4. attach only selected supporting files
|
||||
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://<name>/SKILL.md`
|
||||
4. read `_manifest` only if supporting detail may be needed
|
||||
5. read only selected supporting files
|
||||
|
||||
For manual context attachment, browse the server's resources and attach the same bounded set of files.
|
||||
|
||||
## Prompt Examples
|
||||
|
||||
@@ -72,13 +102,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 browse MCP resources and select one `skill://<name>/SKILL.md` resource by description.
|
||||
3. Read `_manifest` only when supporting material is needed.
|
||||
2. Otherwise call `search_skills` and select one `skill://<name>/SKILL.md` result by description.
|
||||
3. Call `read_resource` for 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.
|
||||
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.
|
||||
|
||||
## Prompt Objects
|
||||
|
||||
@@ -88,8 +118,9 @@ 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. Restart the MCP server after changing skill files because production uses `reload=False`.
|
||||
4. Reload the VS Code window if the server is healthy but the resource picker remains stale.
|
||||
3. Confirm `search_skills` and `read_resource` appear in the chat tool picker when autonomous retrieval is required.
|
||||
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.
|
||||
|
||||
## Further Reading
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ uv sync
|
||||
Run the app locally with the static docs rebuilt first, using [Uvicorn factory mode](https://www.uvicorn.org/settings/#application):
|
||||
|
||||
```bash
|
||||
uv run zensical build && uv run uvicorn personal_mcp.main:create_app --factory --host 127.0.0.1 --port 8765
|
||||
uv run zensical build && uv run uvicorn personal_mcp.web.app:create_app --factory --host 127.0.0.1 --port 8765
|
||||
```
|
||||
|
||||
Build and run the Docker image with the same exposed port:
|
||||
|
||||
@@ -7,6 +7,8 @@ Use this page for MCP client setup, operational tools, and integration reference
|
||||
!!! info "VS Code MCP docs"
|
||||
- [VS Code MCP servers overview](https://code.visualstudio.com/docs/agent-customization/mcp-servers)
|
||||
- [VS Code MCP configuration reference](https://code.visualstudio.com/docs/agents/reference/mcp-configuration)
|
||||
- [VS Code MCP developer guide](https://code.visualstudio.com/docs/agents/guides/mcp-developer-guide)
|
||||
- [VS Code MCP Apps support](https://code.visualstudio.com/blogs/2026/01/26/mcp-apps-support)
|
||||
- [VS Code Copilot customization overview](https://code.visualstudio.com/docs/copilot/customization/overview)
|
||||
|
||||
## Debugging and Inspection
|
||||
|
||||
@@ -13,6 +13,13 @@ 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 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 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 PyPI package](https://pypi.org/project/fastmcp/)
|
||||
|
||||
@@ -54,6 +54,7 @@ Load [styling and customization](./references/styling-and-customization.md) for:
|
||||
|
||||
- progressive discovery through NiceGUI docs, constructors, and Quasar docs
|
||||
- Quasar props, slots, events, and NiceGUI customization methods
|
||||
- app-wide and page-level color themes, dark mode, and semantic CSS tokens
|
||||
- Tailwind for structural styling and static stylesheets for fine tuning
|
||||
- responsive layout and static asset conventions
|
||||
- Tailwind and Quasar breakpoint scales, container queries, and responsive testing
|
||||
|
||||
@@ -71,6 +71,106 @@ with ui.select(
|
||||
|
||||
Prefer constructor arguments when NiceGUI exposes the behavior directly. Use `.props()` for supported Quasar features that are not constructor parameters. Use slots when the Quasar docs define a semantic insertion point; do not reproduce that content with absolute positioning.
|
||||
|
||||
## Application Themes With NiceGUI And Quasar
|
||||
|
||||
Treat a theme as three related layers with different owners:
|
||||
|
||||
1. Configure Quasar's named color roles through NiceGUI.
|
||||
2. Let Quasar own light, dark, and automatic mode state.
|
||||
3. Define application semantic tokens for surfaces and content not covered by Quasar components.
|
||||
|
||||
Do not implement a parallel theme switch by replacing Quasar classes or directly restyling each component. NiceGUI's color APIs set the supported Quasar `--q-*` custom properties, so Quasar components, `color=` arguments, and classes such as `text-primary` and `bg-positive` stay aligned.
|
||||
|
||||
### Set The App-Wide Palette Once
|
||||
|
||||
Use [`app.colors()`](https://nicegui.io/documentation/colors#app-wide-colors) in the composition layer for the default palette. Prefer Quasar's semantic roles over shade names: `primary`, `secondary`, `accent`, `positive`, `negative`, `info`, and `warning`. The `dark` and `dark_page` arguments configure dark surface colors; they do not enable dark mode.
|
||||
|
||||
```python
|
||||
from nicegui import app, ui
|
||||
|
||||
app.colors(
|
||||
primary="#176b5b",
|
||||
secondary="#52645f",
|
||||
accent="#c05a32",
|
||||
dark="#202523",
|
||||
dark_page="#151917",
|
||||
positive="#2e7d32",
|
||||
negative="#b3261e",
|
||||
info="#276b8e",
|
||||
warning="#a86600",
|
||||
brand="#176b5b",
|
||||
)
|
||||
|
||||
|
||||
@ui.page("/")
|
||||
def index() -> None:
|
||||
ui.button("Save")
|
||||
ui.label("Current workspace").classes("text-brand")
|
||||
|
||||
|
||||
ui.run()
|
||||
```
|
||||
|
||||
Custom names such as `brand` become Quasar color names and can be used through `color="brand"`, `text-brand`, or `bg-brand`. Register them before any component uses them. `app.colors()` was added in NiceGUI 3.6.0; for an older pinned version, centralize the same `ui.colors(...)` call in a shared page shell.
|
||||
|
||||
Use [`ui.colors()`](https://nicegui.io/documentation/colors) only when one page intentionally overrides the app palette. It is page-scoped and takes precedence over `app.colors()`:
|
||||
|
||||
```python
|
||||
@ui.page("/operations")
|
||||
def operations_page() -> None:
|
||||
ui.colors(primary="#8f3d2c")
|
||||
ui.button("Operations action")
|
||||
```
|
||||
|
||||
Avoid scattering `ui.colors()` calls among reusable components. A component should consume semantic roles from its owning page rather than silently changing the palette for the whole page.
|
||||
|
||||
### Let Quasar Control Light And Dark Mode
|
||||
|
||||
Use [`ui.dark_mode()`](https://nicegui.io/documentation/dark_mode) for page mode. Its value is tri-state: `True` enables dark mode, `False` disables it, and `None` follows the client's `prefers-color-scheme` setting. It overrides the `dark` default supplied to `ui.run()` or `@ui.page` for that page.
|
||||
|
||||
```python
|
||||
dark_mode = ui.dark_mode(None)
|
||||
|
||||
with ui.button_group():
|
||||
ui.button("System", on_click=dark_mode.auto)
|
||||
ui.button("Light", on_click=dark_mode.disable)
|
||||
ui.button("Dark", on_click=dark_mode.enable)
|
||||
```
|
||||
|
||||
Quasar applies `body--light` or `body--dark`, updates its dark-aware components, and tracks system changes while mode is automatic. Use the NiceGUI element instead of invoking Quasar's JavaScript Dark plugin directly. Persist an explicit user preference separately when it must survive navigation or a new browser session.
|
||||
|
||||
### Add Semantic Tokens For Application Surfaces
|
||||
|
||||
Quasar's brand roles cover framework components, not every application-specific surface. Define a small set of semantic CSS variables in the static stylesheet and change their values under Quasar's documented `.body--dark` class:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--app-page: #f6f8f7;
|
||||
--app-surface: #ffffff;
|
||||
--app-text: #202623;
|
||||
--app-border: #cbd4d0;
|
||||
}
|
||||
|
||||
.body--dark {
|
||||
--app-page: var(--q-dark-page);
|
||||
--app-surface: var(--q-dark);
|
||||
--app-text: #eef3f0;
|
||||
--app-border: #46504b;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--app-page);
|
||||
color: var(--app-text);
|
||||
}
|
||||
|
||||
.app-panel {
|
||||
background: var(--app-surface);
|
||||
border: 1px solid var(--app-border);
|
||||
}
|
||||
```
|
||||
|
||||
Name tokens by purpose, such as `--app-surface` or `--app-muted-text`, rather than by a fixed color such as `--app-gray-100`. Reuse `--q-primary` and the other Quasar variables when the meaning matches. Check text, icon, border, focus, hover, disabled, positive, warning, and negative contrast in both modes; a palette is not complete merely because the page background changes.
|
||||
|
||||
## Structural Styling With Tailwind
|
||||
|
||||
Use standard [Tailwind utility classes](https://tailwindcss.com/docs/utility-first) for page and component structure:
|
||||
@@ -327,7 +427,11 @@ Confirm that page sections do not overlap, toolbars wrap on mobile, desktop pane
|
||||
!!! info "Primary sources"
|
||||
- [NiceGUI element styling and props](https://nicegui.io/documentation/element)
|
||||
- [NiceGUI binding properties](https://nicegui.io/documentation/section_binding_properties)
|
||||
- [NiceGUI color theming](https://nicegui.io/documentation/colors)
|
||||
- [NiceGUI dark mode](https://nicegui.io/documentation/dark_mode)
|
||||
- [Quasar components](https://quasar.dev/vue-components)
|
||||
- [Quasar color palette and runtime brand variables](https://quasar.dev/style/color-palette)
|
||||
- [Quasar dark mode](https://quasar.dev/style/dark-mode)
|
||||
- [Quasar field](https://quasar.dev/vue-components/field/)
|
||||
- [Quasar select](https://quasar.dev/vue-components/select/)
|
||||
- [Tailwind responsive design](https://tailwindcss.com/docs/responsive-design)
|
||||
|
||||
@@ -53,14 +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. a slash-command prompt that names a specific native skill URI
|
||||
2. the `search_skills` and `read_resource` tools for autonomous agents
|
||||
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.
|
||||
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.
|
||||
|
||||
A reliable prompt is:
|
||||
|
||||
```text
|
||||
Browse personal-mcp resources and select the best matching skill://.../SKILL.md resource. Read one selected skill, inspect its _manifest only if supporting detail is needed, and reconcile the guidance with this workspace.
|
||||
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.
|
||||
```
|
||||
|
||||
## Thin Shim Pattern
|
||||
|
||||
+158
-2
@@ -1,24 +1,103 @@
|
||||
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 personal_mcp.prompts import create_prompts_provider
|
||||
from personal_mcp.prompts.models import MarkdownPrompt
|
||||
from personal_mcp.prompts.provider import MarkdownPromptsProvider
|
||||
from personal_mcp.registry.load import get_docs_registry
|
||||
from personal_mcp.registry.load import read_docs_markdown_path
|
||||
from personal_mcp.registry.models import DocsRegistry
|
||||
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://<name>/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.
|
||||
"""
|
||||
_SERVER_ICON = Icon(
|
||||
src=(
|
||||
"data:image/svg+xml;base64,"
|
||||
"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+"
|
||||
"PHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiByeD0iOCIgZmlsbD0iIzE2N0Q4RCIvPjxwYXRoIGQ9"
|
||||
"Ik0xOCAxNmgyMGw4IDh2MjRIMTh6IiBmaWxsPSJub25lIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lk"
|
||||
"dGg9IjQiLz48cGF0aCBkPSJNMzggMTZ2MTBoOE0yNSAzM2gxNE0yNSA0MGgxNCIgZmlsbD0ibm9uZSIg"
|
||||
"c3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSI0Ii8+PC9zdmc+"
|
||||
),
|
||||
mime_type="image/svg+xml",
|
||||
sizes=["64x64"],
|
||||
)
|
||||
|
||||
|
||||
def _ro_annotations() -> dict[str, bool]:
|
||||
return {
|
||||
"readOnlyHint": True,
|
||||
"idempotentHint": True,
|
||||
"openWorldHint": False,
|
||||
}
|
||||
|
||||
|
||||
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*}",
|
||||
name="docs_markdown",
|
||||
title="Documentation Markdown",
|
||||
description="Read a packaged documentation page by its path relative to the docs root.",
|
||||
mime_type="text/markdown",
|
||||
tags={"docs"},
|
||||
annotations=_ro_annotations(),
|
||||
@@ -28,8 +107,54 @@ def _register_components(mcp: FastMCP, registry: DocsRegistry) -> None:
|
||||
|
||||
|
||||
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."
|
||||
),
|
||||
@@ -61,6 +186,7 @@ def _register_resource_tools(mcp: FastMCP) -> None:
|
||||
|
||||
@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(),
|
||||
)
|
||||
@@ -69,11 +195,41 @@ def _register_resource_tools(mcp: FastMCP) -> None:
|
||||
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(
|
||||
ref: object,
|
||||
argument: CompletionArgument,
|
||||
context: CompletionContext | None,
|
||||
) -> list[str] | None:
|
||||
del context
|
||||
if not isinstance(ref, PromptReference):
|
||||
return None
|
||||
|
||||
prompt = await provider.get_prompt(ref.name)
|
||||
if not isinstance(prompt, MarkdownPrompt):
|
||||
return None
|
||||
|
||||
definition = prompt.definitions.get(argument.name)
|
||||
if definition is None or definition.choices is None:
|
||||
return None
|
||||
|
||||
prefix = argument.value.casefold()
|
||||
return [choice for choice in definition.choices if choice.casefold().startswith(prefix)]
|
||||
|
||||
|
||||
def create_mcp() -> FastMCP:
|
||||
registry = get_docs_registry()
|
||||
mcp = FastMCP("personal-mcp", on_duplicate="error")
|
||||
mcp = FastMCP(
|
||||
"personal-mcp",
|
||||
instructions=_SERVER_INSTRUCTIONS,
|
||||
icons=[_SERVER_ICON],
|
||||
on_duplicate="error",
|
||||
)
|
||||
_register_components(mcp, registry)
|
||||
mcp.add_provider(create_prompts_provider())
|
||||
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
|
||||
|
||||
@@ -65,6 +65,7 @@ class MarkdownPrompt(Prompt):
|
||||
metadata = definition.metadata
|
||||
return cls(
|
||||
name=definition.prompt_id,
|
||||
title=definition.prompt_id.replace("-", " ").title(),
|
||||
version=metadata.version,
|
||||
description=metadata.description,
|
||||
tags=set(metadata.tags),
|
||||
|
||||
@@ -35,12 +35,23 @@ class TestMcpHttpEndpoints:
|
||||
"""Covers MCP transport endpoint smoke behavior."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exposes_no_tools(self, mcp_session_factory) -> None:
|
||||
"""Ensures the server publishes only native resource and prompt surfaces."""
|
||||
async def test_tools_bridge_native_skill_resources(self, mcp_session_factory) -> None:
|
||||
"""Ensures tool-only clients can discover and read native skill resources."""
|
||||
async with mcp_session_factory() as mcp_session:
|
||||
result = await mcp_session.list_tools()
|
||||
tools_result = await mcp_session.list_tools()
|
||||
list_result = await mcp_session.call_tool("list_resources")
|
||||
read_result = await mcp_session.call_tool(
|
||||
"read_resource",
|
||||
{"uri": "skill://mcp-details/SKILL.md"},
|
||||
)
|
||||
|
||||
assert result.tools == []
|
||||
assert {tool.name for tool in tools_result.tools} == {
|
||||
"list_resources",
|
||||
"read_resource",
|
||||
"search_skills",
|
||||
}
|
||||
assert "skill://mcp-details/SKILL.md" in list_result.content[0].text
|
||||
assert "# MCP Details" in read_result.content[0].text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_accepts_initialize_jsonrpc_request(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from mcp.types import PromptReference
|
||||
|
||||
pytestmark = pytest.mark.smoke
|
||||
|
||||
@@ -18,6 +19,21 @@ EXPECTED_PROMPTS = {
|
||||
class TestMcpPromptSurface:
|
||||
"""Covers smoke-level MCP prompt discovery and retrieval paths."""
|
||||
|
||||
class TestServerMetadata:
|
||||
"""Covers client-facing identity and completion capabilities."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_advertises_instructions_and_completions(self, mcp_session_factory) -> None:
|
||||
"""Ensures VS Code receives server guidance and completion support."""
|
||||
async with mcp_session_factory(initialize=False) as mcp_session:
|
||||
result = await mcp_session.initialize()
|
||||
|
||||
assert result.instructions is not None
|
||||
assert "skill://<name>/SKILL.md" in result.instructions
|
||||
assert result.server_info.icons
|
||||
assert result.server_info.icons[0].src.startswith("data:image/svg+xml;base64,")
|
||||
assert result.capabilities.completions is not None
|
||||
|
||||
class TestPromptDiscovery:
|
||||
"""Covers MCP prompts/list behavior using native prompt objects."""
|
||||
|
||||
@@ -29,6 +45,18 @@ class TestMcpPromptSurface:
|
||||
|
||||
assert {prompt.name for prompt in result.prompts} == EXPECTED_PROMPTS
|
||||
assert all(prompt.description for prompt in result.prompts)
|
||||
assert all(prompt.title for prompt in result.prompts)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_completes_authored_prompt_choices(self, mcp_session_factory) -> None:
|
||||
"""Ensures finite prompt choices are available as IDE-style suggestions."""
|
||||
async with mcp_session_factory() as mcp_session:
|
||||
result = await mcp_session.complete(
|
||||
PromptReference(type="ref/prompt", name="authoring"),
|
||||
{"name": "artifact_type", "value": "pr"},
|
||||
)
|
||||
|
||||
assert result.completion.values == ["prompt"]
|
||||
|
||||
class TestPromptResolution:
|
||||
"""Covers MCP prompts/get behavior using native request and response objects."""
|
||||
|
||||
@@ -10,6 +10,50 @@ pytestmark = pytest.mark.smoke
|
||||
class TestMcpSkillsSurface:
|
||||
"""Covers native skill resources over the HTTP MCP surface."""
|
||||
|
||||
class TestCompatibilityTools:
|
||||
"""Covers metadata for tool-only MCP clients."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exposes_safe_human_readable_tools(self, mcp_session_factory) -> None:
|
||||
"""Ensures clients receive display titles and complete safety hints."""
|
||||
async with mcp_session_factory() as mcp_session:
|
||||
result = await mcp_session.list_tools()
|
||||
|
||||
tools = {tool.name: tool for tool in result.tools}
|
||||
assert tools["search_skills"].title == "Search Skills"
|
||||
assert tools["list_resources"].title == "List Resources"
|
||||
assert tools["read_resource"].title == "Read Resource"
|
||||
assert all(tool.annotations is not None for tool in tools.values())
|
||||
assert all(tool.annotations.read_only_hint for tool in tools.values() if tool.annotations is not None)
|
||||
assert all(tool.annotations.idempotent_hint for tool in tools.values() if tool.annotations is not None)
|
||||
assert all(
|
||||
tool.annotations.open_world_hint is False for tool in tools.values() if tool.annotations is not None
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_searches_skill_metadata_without_loading_content(self, mcp_session_factory) -> None:
|
||||
"""Ensures search returns bounded canonical skill pointers ranked by metadata."""
|
||||
async with mcp_session_factory() as mcp_session:
|
||||
result = await mcp_session.call_tool(
|
||||
"search_skills",
|
||||
{"query": "FastMCP protocol", "limit": 1},
|
||||
)
|
||||
|
||||
assert result.is_error is False
|
||||
assert result.structured_content == {
|
||||
"results": [
|
||||
{
|
||||
"name": "mcp-details",
|
||||
"description": (
|
||||
"Reference hub for MCP and FastMCP source documentation links. Use when you need "
|
||||
"authoritative protocol, SDK, transport, and deployment docs without loading broad "
|
||||
"implementation guidance."
|
||||
),
|
||||
"uri": "skill://mcp-details/SKILL.md",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
class TestResources:
|
||||
"""Covers native skill resources, manifests, and file templates."""
|
||||
|
||||
|
||||
+2
-1
@@ -107,7 +107,8 @@ nav = [
|
||||
{ "Overview" = "skills/nicegui/SKILL.md" },
|
||||
{ "App Architecture" = "skills/nicegui/references/architecture.md" },
|
||||
{ "Startup" = "skills/nicegui/references/fastapi-uvicorn-startup.md" },
|
||||
{ "Layout and Style" = "skills/nicegui/references/architecture-and-styling.md" },
|
||||
{ "Styling" = "skills/nicegui/references/styling-and-customization.md" },
|
||||
{ "Special Components" = "skills/nicegui/references/special-component-customization.md" },
|
||||
{ "Binding" = "skills/nicegui/references/binding-dataclasses.md" },
|
||||
{ "Flows" = "skills/nicegui/references/interaction-patterns.md" },
|
||||
{ "Quality" = "skills/nicegui/references/troubleshooting-and-quality-gates.md" },
|
||||
|
||||
Reference in New Issue
Block a user