docs exposed in MCP
This commit is contained in:
@@ -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,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]
|
||||
|
||||
@@ -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
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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.",
|
||||
)
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user