Skip to content

Commit c86dc29

Browse files
authored
Merge pull request #302 from posit-dev/fix-mcp-server-v2
fix: MCP server content fixes
2 parents 27b0d89 + 096ac34 commit c86dc29

10 files changed

Lines changed: 443 additions & 305 deletions

File tree

_freeze/reference/disable_tbl_preview/execute-results/html.json

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

_freeze/reference/enable_tbl_preview/execute-results/html.json

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

_freeze/reference/tbl_explorer/execute-results/html.json

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

_freeze/reference/tbl_preview/execute-results/html.json

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

_freeze/site_libs/clipboard/clipboard.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

_freeze/user-guide/authoring-qmd-files/execute-results/html.json

Lines changed: 3 additions & 3 deletions
Large diffs are not rendered by default.

_freeze/user-guide/theming/execute-results/html.json

Lines changed: 3 additions & 3 deletions
Large diffs are not rendered by default.

great_docs/_mcp_docs.py

Lines changed: 310 additions & 275 deletions
Large diffs are not rendered by default.

great_docs/_mcp_runner.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""Run an arbitrary MCP server module over stdio.
2+
3+
This is a tiny, version-agnostic launcher used by `great_docs._mcp_docs`
4+
to introspect an MCP server *through the wire protocol* rather than by reaching
5+
into the `mcp` library's internal handler registries (which change between
6+
major releases). The launcher imports the target module, locates its server
7+
instance, and runs it over stdio using whichever `run` shape the installed
8+
`mcp` version exposes.
9+
10+
Usage:
11+
12+
python -m great_docs._mcp_runner <module_path> [<server_var>]
13+
"""
14+
15+
from __future__ import annotations
16+
17+
import asyncio
18+
import importlib
19+
import sys
20+
from typing import Any
21+
22+
23+
def _find_server(module: Any, server_var: str | None) -> Any:
24+
"""Locate the MCP ``Server``/``FastMCP`` instance in a module."""
25+
if server_var:
26+
return getattr(module, server_var, None)
27+
28+
for attr_name in dir(module):
29+
obj = getattr(module, attr_name)
30+
type_name = type(obj).__name__
31+
module_name = type(obj).__module__ or ""
32+
if "mcp" in module_name and type_name in ("Server", "FastMCP"):
33+
return obj
34+
return None
35+
36+
37+
async def _run(module_path: str, server_var: str | None) -> None:
38+
module = importlib.import_module(module_path)
39+
server = _find_server(module, server_var)
40+
if server is None:
41+
raise SystemExit(f"No MCP server instance found in {module_path}")
42+
43+
# FastMCP knows how to run itself over stdio.
44+
if hasattr(server, "run_stdio_async"):
45+
await server.run_stdio_async()
46+
return
47+
48+
# FastMCP exposes the underlying low-level server as ``_mcp_server``.
49+
low = getattr(server, "_mcp_server", server)
50+
51+
from mcp.server.stdio import stdio_server
52+
53+
async with stdio_server() as (read_stream, write_stream):
54+
# mcp v1 requires initialization options; v2's ``run`` takes only the
55+
# streams. Try the v1 shape first and fall back on signature mismatch.
56+
init_options = None
57+
if hasattr(low, "create_initialization_options"):
58+
try:
59+
init_options = low.create_initialization_options()
60+
except Exception:
61+
init_options = None
62+
63+
if init_options is not None:
64+
try:
65+
await low.run(read_stream, write_stream, init_options)
66+
return
67+
except TypeError:
68+
pass
69+
70+
await low.run(read_stream, write_stream)
71+
72+
73+
if __name__ == "__main__":
74+
module = sys.argv[1] if len(sys.argv) > 1 else ""
75+
var = sys.argv[2] if len(sys.argv) > 2 and sys.argv[2] else None
76+
if not module:
77+
raise SystemExit("usage: python -m great_docs._mcp_runner <module> [<var>]")
78+
asyncio.run(_run(module, var))

great_docs/mcp.py

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -966,7 +966,7 @@ async def list_resources() -> list[Resource]:
966966
resources.append(
967967
Resource(
968968
name="configuration",
969-
uri=AnyUrl("gd://config"),
969+
uri="gd://config",
970970
description="Current Great Docs configuration (great-docs.yml).",
971971
mimeType="text/yaml",
972972
)
@@ -976,7 +976,7 @@ async def list_resources() -> list[Resource]:
976976
resources.append(
977977
Resource(
978978
name="build-log",
979-
uri=AnyUrl("gd://build-log"),
979+
uri="gd://build-log",
980980
description=(
981981
"Most recent build log output. Shows step-by-step build progress, "
982982
"warnings, and errors."
@@ -989,7 +989,7 @@ async def list_resources() -> list[Resource]:
989989
resources.append(
990990
Resource(
991991
name="api-surface",
992-
uri=AnyUrl("gd://api-surface"),
992+
uri="gd://api-surface",
993993
description=(
994994
"Discovered public API exports for the current package. "
995995
"Lists classes, functions, constants, and their categorization."
@@ -1002,7 +1002,7 @@ async def list_resources() -> list[Resource]:
10021002
resources.append(
10031003
Resource(
10041004
name="project-status",
1005-
uri=AnyUrl("gd://status"),
1005+
uri="gd://status",
10061006
description=(
10071007
"Current project documentation status: package info, "
10081008
"configuration state, build artifacts, and enabled features."
@@ -1017,7 +1017,7 @@ async def list_resources() -> list[Resource]:
10171017
resources.append(
10181018
Resource(
10191019
name="pyproject",
1020-
uri=AnyUrl("gd://pyproject"),
1020+
uri="gd://pyproject",
10211021
description="Project metadata from pyproject.toml.",
10221022
mimeType="text/plain",
10231023
)
@@ -1253,38 +1253,65 @@ async def handle_completion(
12531253
if not _MCP_V1 and _V2_HANDLERS:
12541254
_raw = _V2_HANDLERS
12551255

1256+
# mcp v2 handlers must return a typed result model (or dict), not the bare
1257+
# list/str/Completion the v1 decorator API accepted — the v1 decorators used
1258+
# to wrap those for us. Wrap each return value in the appropriate result
1259+
# type so the server speaks valid v2 protocol to real clients.
1260+
from mcp.types import (
1261+
CallToolResult,
1262+
CompleteResult,
1263+
ListPromptsResult,
1264+
ListResourcesResult,
1265+
ListResourceTemplatesResult,
1266+
ListToolsResult,
1267+
ReadResourceResult,
1268+
TextResourceContents,
1269+
)
1270+
from mcp.types import (
1271+
Completion as _Completion,
1272+
)
1273+
12561274
async def _on_list_tools(ctx: Any, params: Any = None) -> Any:
1257-
return await _raw["list_tools"]()
1275+
return ListToolsResult(tools=await _raw["list_tools"]())
12581276

12591277
async def _on_call_tool(ctx: Any, params: Any) -> Any:
1260-
return await _raw["call_tool"](
1278+
content = await _raw["call_tool"](
12611279
getattr(params, "name", ""),
12621280
getattr(params, "arguments", {}) or {},
12631281
)
1282+
return CallToolResult(content=list(content or []))
12641283

12651284
async def _on_list_prompts(ctx: Any, params: Any = None) -> Any:
1266-
return await _raw["list_prompts"]()
1285+
return ListPromptsResult(prompts=await _raw["list_prompts"]())
12671286

12681287
async def _on_get_prompt(ctx: Any, params: Any) -> Any:
1288+
# The v1 handler already returns a GetPromptResult (a valid v2 model).
12691289
return await _raw["get_prompt"](
12701290
getattr(params, "name", ""),
12711291
getattr(params, "arguments", None),
12721292
)
12731293

12741294
async def _on_list_resources(ctx: Any, params: Any = None) -> Any:
1275-
return await _raw["list_resources"]()
1295+
return ListResourcesResult(resources=await _raw["list_resources"]())
12761296

12771297
async def _on_read_resource(ctx: Any, params: Any) -> Any:
1278-
return await _raw["read_resource"](getattr(params, "uri", ""))
1298+
uri = getattr(params, "uri", "")
1299+
text = await _raw["read_resource"](uri)
1300+
return ReadResourceResult(contents=[TextResourceContents(uri=uri, text=text or "")])
12791301

12801302
async def _on_list_resource_templates(ctx: Any, params: Any = None) -> Any:
1281-
return await _raw["list_resource_templates"]()
1303+
return ListResourceTemplatesResult(
1304+
resourceTemplates=await _raw["list_resource_templates"]()
1305+
)
12821306

12831307
async def _on_completion(ctx: Any, params: Any) -> Any:
1284-
return await _raw["completion"](
1308+
completion = await _raw["completion"](
12851309
getattr(params, "ref", None),
12861310
getattr(params, "argument", None),
12871311
)
1312+
if completion is None:
1313+
completion = _Completion(values=[])
1314+
return CompleteResult(completion=completion)
12881315

12891316
server = Server(
12901317
name="great-docs",
@@ -1312,9 +1339,7 @@ async def run_mcp_server():
13121339
"""Run the Great Docs MCP server over stdio."""
13131340
async with stdio_server() as (read_stream, write_stream):
13141341
if _MCP_V1:
1315-
await server.run(
1316-
read_stream, write_stream, server.create_initialization_options()
1317-
)
1342+
await server.run(read_stream, write_stream, server.create_initialization_options())
13181343
else:
13191344
await server.run(read_stream, write_stream)
13201345

0 commit comments

Comments
 (0)