Resolves https://linear.app/arcadedev/issue/TOO-590/add-resources-support-to-server-framework <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Adds new resource registration/reading semantics (including URI templates and duplicate/multiple-match policies) and changes JSON Schema generation for tool I/O, which may affect MCP client compatibility and runtime behavior across servers. > > **Overview** > **Adds first-class MCP Resources support across `arcade-mcp-server`.** `MCPApp` can now register resources at build time via `add_resource`/`@resource` plus convenience `add_text_resource` and `add_file_resource`, and passes these through to `MCPServer` for startup loading (including `ResourceTemplate` URIs with `{param}` and `{param*}` matching). > > **Extends `ResourceManager` behavior.** Resource reads now coerce handler return types (including raw `bytes` to base64 `BlobResourceContents`), support template matching with overlap/multiple-match detection, and introduce configurable duplicate handling policies. > > **Improves tool schema + MCP Apps linking.** Tool input/output JSON Schema generation is refactored to recursively expand nested `json` schemas and ensure `outputSchema` is always an object (wrapping non-object returns in a `result` property); `MCPApp` also supports attaching arbitrary tool `_meta` extensions (e.g., `ui.resourceUri`) applied at server start. > > Adds two new example servers (`resources`, `tools_with_output_schema`) and broad test coverage for resource templates, static/file resources, meta extensions, and schema wrapping/recursion. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit e785bee79d74110727519b00b81dcad6e9b74212. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
123 lines
3.7 KiB
Python
123 lines
3.7 KiB
Python
"""
|
|
Stdio transport runner for MCP server.
|
|
|
|
Provides the async entry point for running the MCP server over stdio,
|
|
and tool catalog initialization used by both stdio and HTTP modes.
|
|
"""
|
|
|
|
import sys
|
|
from collections.abc import Callable
|
|
from typing import Any
|
|
|
|
from arcade_core.catalog import ToolCatalog
|
|
from arcade_core.discovery import discover_tools
|
|
from arcade_core.toolkit import ToolkitLoadError
|
|
from dotenv import load_dotenv
|
|
from loguru import logger
|
|
|
|
from arcade_mcp_server.server import MCPServer
|
|
from arcade_mcp_server.settings import MCPSettings
|
|
from arcade_mcp_server.types import Resource, ResourceTemplate
|
|
|
|
|
|
def initialize_tool_catalog(
|
|
tool_package: str | None = None,
|
|
show_packages: bool = False,
|
|
discover_installed: bool = False,
|
|
server_name: str | None = None,
|
|
server_version: str | None = None,
|
|
) -> ToolCatalog:
|
|
"""
|
|
Discover and load tools from various sources.
|
|
|
|
Returns a ToolCatalog or exits with a friendly error if nothing found.
|
|
"""
|
|
try:
|
|
catalog = discover_tools(
|
|
tool_package=tool_package,
|
|
show_packages=show_packages,
|
|
discover_installed=discover_installed,
|
|
server_name=server_name,
|
|
server_version=server_version,
|
|
)
|
|
except ToolkitLoadError as exc:
|
|
logger.error(str(exc))
|
|
sys.exit(1)
|
|
|
|
total_tools = len(catalog)
|
|
if total_tools == 0:
|
|
logger.error("No tools found. Create Python files with @tool decorated functions.")
|
|
sys.exit(1)
|
|
|
|
logger.info(f"Total tools loaded: {total_tools}")
|
|
return catalog
|
|
|
|
|
|
async def run_stdio_server(
|
|
catalog: ToolCatalog,
|
|
debug: bool = False,
|
|
env_file: str | None = None,
|
|
settings: MCPSettings | None = None,
|
|
initial_resources: list[tuple[Resource | ResourceTemplate, Callable[..., Any] | None]]
|
|
| None = None,
|
|
tool_meta_extensions: dict[str, dict[str, Any]] | None = None,
|
|
**kwargs: Any,
|
|
) -> None:
|
|
"""Run MCP server with stdio transport."""
|
|
from arcade_mcp_server.transports.stdio import StdioTransport
|
|
|
|
if env_file:
|
|
load_dotenv(env_file)
|
|
logger.debug(f"Loaded environment variables from --env-file={env_file}")
|
|
|
|
if settings is None:
|
|
settings = MCPSettings.from_env()
|
|
|
|
if debug:
|
|
settings.debug = True
|
|
settings.middleware.enable_logging = True
|
|
settings.middleware.log_level = "DEBUG"
|
|
|
|
try:
|
|
tool_env_keys = sorted(settings.tool_secrets().keys())
|
|
logger.debug(
|
|
f"Arcade settings: \n\
|
|
ARCADE_ENVIRONMENT={settings.arcade.environment} \n\
|
|
ARCADE_API_URL={settings.arcade.api_url}, \n\
|
|
ARCADE_USER_ID={settings.arcade.user_id}, \n\
|
|
api_key_present - {bool(settings.arcade.api_key)}"
|
|
)
|
|
logger.debug(f"Tool environment variable names available to tools: {tool_env_keys}")
|
|
except Exception as e:
|
|
logger.debug(f"Unable to log settings/tool env keys: {e}")
|
|
|
|
server = MCPServer(
|
|
catalog=catalog,
|
|
settings=settings,
|
|
initial_resources=initial_resources,
|
|
tool_meta_extensions=tool_meta_extensions,
|
|
**kwargs,
|
|
)
|
|
|
|
transport = StdioTransport()
|
|
|
|
try:
|
|
await server.start()
|
|
await transport.start()
|
|
|
|
async with transport.connect_session() as session:
|
|
await server.run_connection(
|
|
session.read_stream,
|
|
session.write_stream,
|
|
session.init_options,
|
|
)
|
|
except KeyboardInterrupt:
|
|
logger.info("Server stopped by user")
|
|
except Exception as e:
|
|
logger.exception(f"Server error: {e}")
|
|
raise
|
|
finally:
|
|
try:
|
|
await transport.stop()
|
|
finally:
|
|
await server.stop()
|