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 ..admin_surface import register_service_admin_tools
|
||||||
from ..broker.config import build_service_from_config
|
from ..broker.config import build_service_from_config
|
||||||
from ..broker.transport import normalize_transport
|
from ..broker.transport import normalize_transport
|
||||||
|
from ..documentation import build_local_documentation_source
|
||||||
from ..models import BrokerConfig
|
from ..models import BrokerConfig
|
||||||
from ..transparent_proxy.runtime import ProxyRuntime
|
from ..transparent_proxy.runtime import ProxyRuntime
|
||||||
from ..workflow_surface import register_workflow_tools
|
from ..workflow_surface import register_workflow_tools
|
||||||
|
from .resources import register_documentation_resources
|
||||||
|
|
||||||
|
|
||||||
def create_server(
|
def create_server(
|
||||||
@@ -42,6 +44,9 @@ def create_server(
|
|||||||
include_connection_tools=False,
|
include_connection_tools=False,
|
||||||
)
|
)
|
||||||
register_workflow_tools(runtime.server, service)
|
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
|
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 .refs import CapabilityRef, SourceRef
|
||||||
from .paging import Page, page_items
|
from .paging import Page, page_items
|
||||||
from .schema_hashes import hash_json_schema
|
from .schema_hashes import hash_json_schema
|
||||||
@@ -20,6 +21,7 @@ __all__ = [
|
|||||||
"CapabilityBuckets",
|
"CapabilityBuckets",
|
||||||
"CapabilitySource",
|
"CapabilitySource",
|
||||||
"CapabilityRef",
|
"CapabilityRef",
|
||||||
|
"DocumentationResource",
|
||||||
"NodeSpecInventory",
|
"NodeSpecInventory",
|
||||||
"Page",
|
"Page",
|
||||||
"ReducerInventory",
|
"ReducerInventory",
|
||||||
@@ -32,6 +34,7 @@ __all__ = [
|
|||||||
"SourceVisibility",
|
"SourceVisibility",
|
||||||
"SourceVisibilitySnapshot",
|
"SourceVisibilitySnapshot",
|
||||||
"SourceRef",
|
"SourceRef",
|
||||||
|
"build_documentation_source",
|
||||||
"hash_json_schema",
|
"hash_json_schema",
|
||||||
"page_items",
|
"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 (
|
from wf_platform import (
|
||||||
CapabilityBuckets,
|
CapabilityBuckets,
|
||||||
CapabilitySource,
|
CapabilitySource,
|
||||||
|
DocumentationResource,
|
||||||
SourceInventory,
|
SourceInventory,
|
||||||
SourcePermissions,
|
SourcePermissions,
|
||||||
SourceStatus,
|
SourceStatus,
|
||||||
SourceVisibility,
|
SourceVisibility,
|
||||||
|
build_documentation_source,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -51,3 +53,25 @@ def test_capability_source_projects_typed_inventory() -> None:
|
|||||||
assert inventory.model_dump(mode="json")["capabilities"]["tools"] == [
|
assert inventory.model_dump(mode="json")["capabilities"]["tools"] == [
|
||||||
"wf.std.inspect"
|
"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
|
import sys
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from mcp import types as mcp_types
|
||||||
|
|
||||||
from wf_mcp.models import BrokerConfig, ConnectionConfig
|
from wf_mcp.models import BrokerConfig, ConnectionConfig
|
||||||
from wf_mcp.server import create_server_client
|
from wf_mcp.server import create_server_client
|
||||||
|
|
||||||
@@ -181,6 +183,26 @@ def test_workflow_tools_have_human_metadata() -> None:
|
|||||||
asyncio.run(run_proxy())
|
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:
|
def test_admin_tools_have_human_metadata() -> None:
|
||||||
config = BrokerConfig(
|
config = BrokerConfig(
|
||||||
store_root=local_temp_root() / "unified_admin_metadata_store",
|
store_root=local_temp_root() / "unified_admin_metadata_store",
|
||||||
|
|||||||
Reference in New Issue
Block a user