search_skills
This commit is contained in:
@@ -60,6 +60,8 @@ For skills:
|
|||||||
4. read `_manifest` when supporting material may be needed
|
4. read `_manifest` when supporting material may be needed
|
||||||
5. fetch only the supporting paths relevant to the task
|
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 or their generic tool projection.
|
||||||
|
|
||||||
## Stability Policy
|
## Stability Policy
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ Copilot interacts with MCP servers through independently exposed lanes:
|
|||||||
2. resources attached as read-only context
|
2. resources attached as read-only context
|
||||||
3. server-provided prompts
|
3. server-provided prompts
|
||||||
|
|
||||||
This server publishes skills as native `skill://` resources and prompts as native MCP prompt objects. It also exposes generic `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 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
|
## VS Code Feature Coverage
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ For every skill, Copilot can discover:
|
|||||||
2. `skill://<name>/_manifest`
|
2. `skill://<name>/_manifest`
|
||||||
3. `skill://<name>/{path*}` supporting-file template
|
3. `skill://<name>/{path*}` supporting-file template
|
||||||
|
|
||||||
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 generic tools delegate to that same resource surface rather than maintaining a parallel 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
|
## Resource Picker Availability
|
||||||
|
|
||||||
@@ -66,8 +66,8 @@ A successful `resources/list` response does not guarantee the picker appears in
|
|||||||
|
|
||||||
For autonomous agents:
|
For autonomous agents:
|
||||||
|
|
||||||
1. call `list_resources`
|
1. call `search_skills` with the task, capability, or technology
|
||||||
2. compare main skill names and descriptions
|
2. compare the bounded main-skill matches
|
||||||
3. call `read_resource` for one relevant `skill://<name>/SKILL.md`
|
3. call `read_resource` for one relevant `skill://<name>/SKILL.md`
|
||||||
4. read `_manifest` only if supporting detail may be needed
|
4. read `_manifest` only if supporting detail may be needed
|
||||||
5. read only selected supporting files
|
5. read only selected supporting files
|
||||||
@@ -102,7 +102,7 @@ A repo-level instruction should name the native retrieval order and context budg
|
|||||||
When a task matches a personal-mcp skill:
|
When a task matches a personal-mcp skill:
|
||||||
|
|
||||||
1. Prefer an already attached native skill resource.
|
1. Prefer an already attached native skill resource.
|
||||||
2. Otherwise call `list_resources` and select one `skill://<name>/SKILL.md` resource by description.
|
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.
|
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.
|
4. Load at most two candidate main files and only the relevant supporting paths.
|
||||||
5. Reconcile guidance with the current repository before editing.
|
5. Reconcile guidance with the current repository before editing.
|
||||||
@@ -118,7 +118,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.
|
1. Use `MCP: List Servers` to confirm the server is enabled.
|
||||||
2. Use `MCP: Browse Resources` to confirm native skill resources exist.
|
2. Use `MCP: Browse Resources` to confirm native skill resources exist.
|
||||||
3. Confirm `list_resources` and `read_resource` appear in the chat tool picker when autonomous retrieval is required.
|
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`.
|
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.
|
5. Reload the VS Code window if the server is healthy but the resource or tool picker remains stale.
|
||||||
|
|
||||||
|
|||||||
@@ -53,15 +53,15 @@ These utilities operate directly on the native `skill://` contract and require n
|
|||||||
In VS Code, skills can arrive through:
|
In VS Code, skills can arrive through:
|
||||||
|
|
||||||
1. explicit attachment from `Add Context > MCP Resources` or `MCP: Browse Resources`
|
1. explicit attachment from `Add Context > MCP Resources` or `MCP: Browse Resources`
|
||||||
2. the generic `list_resources` and `read_resource` tools for autonomous agents
|
2. the `search_skills` and `read_resource` tools for autonomous agents
|
||||||
3. a slash-command prompt that names a specific native skill URI
|
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 generic tools; they delegate to the native providers and do not duplicate skill metadata or content.
|
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:
|
A reliable prompt is:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Call list_resources and select the best matching skill://.../SKILL.md resource. Use read_resource for 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
|
## Thin Shim Pattern
|
||||||
|
|||||||
+98
-3
@@ -1,10 +1,15 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
from fastmcp import FastMCP
|
from fastmcp import FastMCP
|
||||||
from mcp.types import CompletionArgument
|
from mcp.types import CompletionArgument
|
||||||
from mcp.types import CompletionContext
|
from mcp.types import CompletionContext
|
||||||
from mcp.types import Icon
|
from mcp.types import Icon
|
||||||
from mcp.types import PromptReference
|
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 import create_prompts_provider
|
||||||
from personal_mcp.prompts.models import MarkdownPrompt
|
from personal_mcp.prompts.models import MarkdownPrompt
|
||||||
@@ -16,9 +21,9 @@ from personal_mcp.skills import create_skills_provider
|
|||||||
|
|
||||||
_SERVER_INSTRUCTIONS = """Personal development guidance exposed as native MCP resources and prompts.
|
_SERVER_INSTRUCTIONS = """Personal development guidance exposed as native MCP resources and prompts.
|
||||||
|
|
||||||
Use prompts for parameterized workflows. For task-specific guidance, list resources, select one
|
Use prompts for parameterized workflows. For task-specific guidance, search skills, select one
|
||||||
skill://<name>/SKILL.md by description, and read its manifest only when supporting detail is needed.
|
skill://<name>/SKILL.md result, and read its manifest only when supporting detail is needed. Read-only
|
||||||
Read-only compatibility tools expose the same resource catalog to clients without native resource access.
|
compatibility tools expose the same resource catalog to clients without native resource access.
|
||||||
"""
|
"""
|
||||||
_SERVER_ICON = Icon(
|
_SERVER_ICON = Icon(
|
||||||
src=(
|
src=(
|
||||||
@@ -42,6 +47,51 @@ 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:
|
def _register_components(mcp: FastMCP, registry: DocsRegistry) -> None:
|
||||||
@mcp.resource(
|
@mcp.resource(
|
||||||
"resource://docs/{path*}",
|
"resource://docs/{path*}",
|
||||||
@@ -57,6 +107,51 @@ def _register_components(mcp: FastMCP, registry: DocsRegistry) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _register_resource_tools(mcp: FastMCP) -> 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(
|
@mcp.tool(
|
||||||
name="list_resources",
|
name="list_resources",
|
||||||
title="List Resources",
|
title="List Resources",
|
||||||
|
|||||||
@@ -35,12 +35,23 @@ class TestMcpHttpEndpoints:
|
|||||||
"""Covers MCP transport endpoint smoke behavior."""
|
"""Covers MCP transport endpoint smoke behavior."""
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_exposes_no_tools(self, mcp_session_factory) -> None:
|
async def test_tools_bridge_native_skill_resources(self, mcp_session_factory) -> None:
|
||||||
"""Ensures the server publishes only native resource and prompt surfaces."""
|
"""Ensures tool-only clients can discover and read native skill resources."""
|
||||||
async with mcp_session_factory() as mcp_session:
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_accepts_initialize_jsonrpc_request(
|
async def test_accepts_initialize_jsonrpc_request(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from mcp.types import PromptReference
|
||||||
|
|
||||||
pytestmark = pytest.mark.smoke
|
pytestmark = pytest.mark.smoke
|
||||||
|
|
||||||
@@ -18,6 +19,21 @@ EXPECTED_PROMPTS = {
|
|||||||
class TestMcpPromptSurface:
|
class TestMcpPromptSurface:
|
||||||
"""Covers smoke-level MCP prompt discovery and retrieval paths."""
|
"""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:
|
class TestPromptDiscovery:
|
||||||
"""Covers MCP prompts/list behavior using native prompt objects."""
|
"""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 {prompt.name for prompt in result.prompts} == EXPECTED_PROMPTS
|
||||||
assert all(prompt.description for prompt in result.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:
|
class TestPromptResolution:
|
||||||
"""Covers MCP prompts/get behavior using native request and response objects."""
|
"""Covers MCP prompts/get behavior using native request and response objects."""
|
||||||
|
|||||||
@@ -10,6 +10,50 @@ pytestmark = pytest.mark.smoke
|
|||||||
class TestMcpSkillsSurface:
|
class TestMcpSkillsSurface:
|
||||||
"""Covers native skill resources over the HTTP MCP surface."""
|
"""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:
|
class TestResources:
|
||||||
"""Covers native skill resources, manifests, and file templates."""
|
"""Covers native skill resources, manifests, and file templates."""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user