docs exposed in MCP

This commit is contained in:
lda
2026-05-18 17:13:12 +07:00 Verified
parent 7fe0127c90
commit 4d90101e4b
7 changed files with 180 additions and 0 deletions
+54
View File
@@ -0,0 +1,54 @@
from __future__ import annotations
from pathlib import Path
from wf_platform import CapabilitySource, DocumentationResource, build_documentation_source
def build_local_documentation_source(repo_root: Path) -> CapabilitySource:
"""Load project manuals into a provider-neutral local documentation source."""
docs_dir = repo_root / "docs"
return build_documentation_source(
[
_markdown_resource(
path=docs_dir / "wf_mcp_operator_manual.md",
name="wf.docs.operator_manual",
uri="wf://docs/operator-manual",
title="wf_mcp Operator Manual",
description="Short mental model and tool-family map for wf_mcp.",
),
_markdown_resource(
path=docs_dir / "wf_mcp_end_to_end_runbook.md",
name="wf.docs.end_to_end_runbook",
uri="wf://docs/end-to-end-runbook",
title="wf_mcp End-To-End Runbook",
description="Connection-to-deployment workflow runbook.",
),
_markdown_resource(
path=docs_dir / "wf_mcp_troubleshooting.md",
name="wf.docs.troubleshooting",
uri="wf://docs/troubleshooting",
title="wf_mcp Troubleshooting",
description="Failure-oriented guide for discovery and deployment issues.",
),
]
)
def _markdown_resource(
*,
path: Path,
name: str,
uri: str,
title: str,
description: str,
) -> DocumentationResource:
"""Load one Markdown manual into the transport-neutral docs model."""
return DocumentationResource(
name=name,
uri=uri,
title=title,
description=description,
mime_type="text/markdown",
text=path.read_text(encoding="utf-8"),
)
+10
View File
@@ -10,9 +10,11 @@ from fastmcp.client.transports.memory import FastMCPTransport
from ..admin_surface import register_service_admin_tools
from ..broker.config import build_service_from_config
from ..broker.transport import normalize_transport
from ..documentation import build_local_documentation_source
from ..models import BrokerConfig
from ..transparent_proxy.runtime import ProxyRuntime
from ..workflow_surface import register_workflow_tools
from .resources import register_documentation_resources
def create_server(
@@ -42,6 +44,9 @@ def create_server(
include_connection_tools=False,
)
register_workflow_tools(runtime.server, service)
docs_source = build_local_documentation_source(_repo_root())
service.capability_sources[docs_source.id] = docs_source
register_documentation_resources(runtime.server, docs_source)
return runtime.server
@@ -87,3 +92,8 @@ def create_server_client(
)
)
)
def _repo_root() -> Path:
"""Return the project root while docs still live beside the source tree."""
return Path(__file__).resolve().parents[3]
+35
View File
@@ -0,0 +1,35 @@
from __future__ import annotations
from typing import Any
from fastmcp import FastMCP
from wf_platform import CapabilitySource, DocumentationResource
def register_documentation_resources(
server: FastMCP[Any],
source: CapabilitySource,
) -> None:
"""Project provider-neutral documentation resources through MCP."""
for resource in source.capabilities.resources.values():
if not isinstance(resource, DocumentationResource):
continue
_register_documentation_resource(server, resource)
def _register_documentation_resource(
server: FastMCP[Any],
resource: DocumentationResource,
) -> None:
"""Bind one stable docs URI to its stored Markdown text."""
@server.resource(
resource.uri,
name=resource.name,
title=resource.title,
description=resource.description,
mime_type=resource.mime_type,
)
def documentation_resource() -> str:
return resource.text
+3
View File
@@ -1,3 +1,4 @@
from .docs import DocumentationResource, build_documentation_source
from .refs import CapabilityRef, SourceRef
from .paging import Page, page_items
from .schema_hashes import hash_json_schema
@@ -20,6 +21,7 @@ __all__ = [
"CapabilityBuckets",
"CapabilitySource",
"CapabilityRef",
"DocumentationResource",
"NodeSpecInventory",
"Page",
"ReducerInventory",
@@ -32,6 +34,7 @@ __all__ = [
"SourceVisibility",
"SourceVisibilitySnapshot",
"SourceRef",
"build_documentation_source",
"hash_json_schema",
"page_items",
]
+32
View File
@@ -0,0 +1,32 @@
from __future__ import annotations
from dataclasses import dataclass
from .sources import CapabilityBuckets, CapabilitySource, SourceVisibility
@dataclass(frozen=True, slots=True)
class DocumentationResource:
"""Provider-neutral text resource intended for humans or authoring clients."""
name: str
uri: str
title: str
description: str
mime_type: str
text: str
def build_documentation_source(
resources: list[DocumentationResource],
) -> CapabilitySource:
"""Build the local documentation source without depending on MCP transport."""
return CapabilitySource(
id="wf.docs",
kind="system",
capabilities=CapabilityBuckets(
resources={resource.name: resource for resource in resources}
),
visibility=SourceVisibility(mcp_client=True),
description="Local operator and workflow documentation.",
)
+24
View File
@@ -3,10 +3,12 @@ from __future__ import annotations
from wf_platform import (
CapabilityBuckets,
CapabilitySource,
DocumentationResource,
SourceInventory,
SourcePermissions,
SourceStatus,
SourceVisibility,
build_documentation_source,
)
@@ -51,3 +53,25 @@ def test_capability_source_projects_typed_inventory() -> None:
assert inventory.model_dump(mode="json")["capabilities"]["tools"] == [
"wf.std.inspect"
]
def test_documentation_source_owns_provider_neutral_resources() -> None:
source = build_documentation_source(
[
DocumentationResource(
name="wf.docs.operator_manual",
uri="wf://docs/operator-manual",
title="Operator Manual",
description="How to operate the platform.",
mime_type="text/markdown",
text="# Operator Manual",
)
]
)
resource = source.capabilities.resources["wf.docs.operator_manual"]
assert source.id == "wf.docs"
assert source.visibility.mcp_client is True
assert resource.uri == "wf://docs/operator-manual"
assert resource.text == "# Operator Manual"
+22
View File
@@ -4,6 +4,8 @@ import asyncio
import sys
from typing import Any
from mcp import types as mcp_types
from wf_mcp.models import BrokerConfig, ConnectionConfig
from wf_mcp.server import create_server_client
@@ -181,6 +183,26 @@ def test_workflow_tools_have_human_metadata() -> None:
asyncio.run(run_proxy())
def test_server_exposes_platform_documentation_resources() -> None:
config = BrokerConfig(
store_root=local_temp_root() / "unified_docs_resource_store",
connections=[],
)
async def run_proxy() -> None:
client = create_server_client(config, admin_tools=False)
async with client:
resources = await client.list_resources()
uris = [str(resource.uri) for resource in resources]
assert "wf://docs/operator-manual" in uris
result = await client.read_resource("wf://docs/operator-manual")
assert isinstance(result[0], mcp_types.TextResourceContents)
assert "wf_mcp Operator Manual" in result[0].text
asyncio.run(run_proxy())
def test_admin_tools_have_human_metadata() -> None:
config = BrokerConfig(
store_root=local_temp_root() / "unified_admin_metadata_store",