MCP stdio Implementation: The PR adds support for standard input/output (stdio) as a transport mechanism for the Message Control Protocol. This is a replacement to the SSE (Server-Sent Events) transport that was worked on in PR #359 but will not be merged as it's not deprecated. This will allow developers to use Arcade tools (written by the dev or Arcade) in Claude, Cursor, windsurf, etc. The engine Gateway already supports adding HTTPS streamable (replacement for SSE) MCP servers as tool servers, and will soon support full gateway capability in the client API as well. To use any existing Toolkit just ## Examples ### Quickstart setup with existing toolkits ```bash pip install arcade-ai pip install <name of toolkit> # ex. arcade-google arcade serve --mcp ``` ### Run with Claude Just add the following to the Claude config ```json { "mcpServers": { "arcade": { "command": "bash", "args": ["-c", "export ARCADE_API_KEY=arc_xxxx && /path/to/python /path/to/arcade serve --mcp"] } } } ``` ### Customizing the Tool Server Developers can customize their served tools and server furthermore by importing the worker sdk ```python import arcade_google # pip install arcade_google import arcade_search # pip install arcade_search from arcade.core.catalog import ToolCatalog from arcade.worker.mcp.stdio import StdioServer # 2. Create and populate the tool catalog catalog = ToolCatalog() catalog.add_module(arcade_google) # Registers all tools in the package catalog.add_module(arcade_search) # 3. Main entrypoint async def main(): # Create the worker with the tool catalog worker = StdioServer(catalog) # Run the worker await worker.run() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` Then to run with claude, just run this python file instead of the prebuilt server used in ``arcade serve --mcp``
44 lines
1.5 KiB
Python
44 lines
1.5 KiB
Python
import json
|
|
from typing import Annotated
|
|
|
|
from arcade.core.catalog import ToolCatalog
|
|
from arcade.sdk import tool
|
|
from arcade.worker.mcp.convert import convert_to_mcp_content, create_mcp_tool
|
|
|
|
|
|
@tool
|
|
def sample_tool(x: Annotated[int, "first"], y: Annotated[int, "second"]) -> int:
|
|
"""Return x+y"""
|
|
|
|
return x + y
|
|
|
|
|
|
def test_convert_to_mcp_content_primitives():
|
|
assert convert_to_mcp_content(42) == [{"type": "text", "text": "42"}]
|
|
assert convert_to_mcp_content("hello") == [{"type": "text", "text": "hello"}]
|
|
assert convert_to_mcp_content(True) == [{"type": "text", "text": "True"}]
|
|
|
|
|
|
def test_convert_to_mcp_content_complex():
|
|
data = {"a": 1}
|
|
expected_json = json.dumps(data)
|
|
assert convert_to_mcp_content(data) == [{"type": "text", "text": expected_json}]
|
|
|
|
|
|
def test_create_mcp_tool():
|
|
# Materialize a tool via catalog then feed it to create_mcp_tool
|
|
catalog = ToolCatalog()
|
|
catalog.add_tool(sample_tool, "convert_toolkit")
|
|
mat_tool = next(iter(catalog)) # only tool
|
|
mcp_tool = create_mcp_tool(mat_tool)
|
|
|
|
assert mcp_tool is not None
|
|
assert mcp_tool["name"] == "ConvertToolkit_SampleTool"
|
|
assert mcp_tool["description"]
|
|
# Ensure input schema contains both parameters and marks them required
|
|
props = mcp_tool["inputSchema"]["properties"]
|
|
assert set(props.keys()) == {"x", "y"}
|
|
|
|
required_fields = set(mcp_tool["inputSchema"].get("required", []))
|
|
# Ensure no unexpected required fields and that declared ones are subset of expected
|
|
assert required_fields.issubset({"x", "y"})
|