@nimbus-dev/create-connector
Advanced tools
+1
-1
| { | ||
| "name": "@nimbus-dev/create-connector", | ||
| "version": "0.2.0", | ||
| "version": "0.3.0", | ||
| "description": "Scaffold a Nimbus MCP connector in TypeScript or Python", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
@@ -15,3 +15,3 @@ [build-system] | ||
| # installs the wrong package rather than failing. | ||
| "nimbus-dev-sdk>=0.3.0", | ||
| "nimbus-dev-sdk>=0.11.0", | ||
| # Ceilinged at the next major: `mcp` 2.0 renamed `FastMCP` to `MCPServer` and moved the | ||
@@ -18,0 +18,0 @@ # module, so a major bump is not a drop-in for `main.py`. |
@@ -15,7 +15,8 @@ """Handshake first, then serve. | ||
| TypeScript wraps the tool registration below in ``connector-kit``'s | ||
| ``createRegisterSimpleTool`` and ``mcpJsonResult``. ``nimbus-dev-sdk`` publishes no | ||
| Python equivalent, so ``_on_list_tools`` / ``_on_call_tool`` / ``_json_result`` — the | ||
| few lines that kit would absorb — sit inline. That is deliberate: a scaffold is not | ||
| where a new published surface gets designed. | ||
| Dispatch is not this file's own: ``ToolRouter``, from ``nimbus_sdk.connector_kit``, owns | ||
| it — the way the TypeScript template leans on ``createRegisterSimpleTool``. What is left | ||
| here is the handshake machinery above and two adapters below, which turn the router's | ||
| wire shapes into ``mcp``'s pydantic models. Those adapters are the only place pydantic | ||
| appears, and they are generic: adding a tool means one more ``ROUTER.add`` call and no | ||
| change to either. | ||
| """ | ||
@@ -25,3 +26,2 @@ | ||
| import json | ||
| import os | ||
@@ -39,2 +39,3 @@ import sys | ||
| from nimbus_sdk import CONTRACT_HANDSHAKE_EXIT | ||
| from nimbus_sdk.connector_kit import McpToolResult, ToolRouter, json_result | ||
| from nimbus_sdk.ipc import HandshakeOk, NdjsonLineReader, perform_handshake | ||
@@ -126,14 +127,30 @@ | ||
| def _json_result(payload: dict[str, str]) -> types.CallToolResult: | ||
| return types.CallToolResult( | ||
| content=[types.TextContent(type="text", text=json.dumps(payload, indent=2))] | ||
| ) | ||
| ROUTER = ToolRouter() | ||
| def _error_result(message: str) -> types.CallToolResult: | ||
| return types.CallToolResult( | ||
| content=[types.TextContent(type="text", text=message)], is_error=True | ||
| ) | ||
| def _validate_echo(args: dict[str, Any]) -> None: | ||
| """Check what ``ECHO_INPUT_SCHEMA`` advertises but nothing enforces. | ||
| ``inputSchema`` is sent to the client and never checked against an actual call: a | ||
| dependency-free SDK carries no JSON Schema implementation, and pretending otherwise | ||
| would be worse than saying so. ``validate`` is the seam that closes the gap — raise | ||
| to reject, and the router turns whatever you raise into an error result. | ||
| """ | ||
| if not isinstance(args.get("text"), str): | ||
| raise ValueError("text must be a string") | ||
| def _echo_tool(args: dict[str, Any]) -> McpToolResult: | ||
| return json_result(echo(args["text"])) | ||
| ROUTER.add( | ||
| TOOLS[0]["name"], | ||
| TOOLS[0]["description"], | ||
| ECHO_INPUT_SCHEMA, | ||
| _echo_tool, | ||
| validate=_validate_echo, | ||
| ) | ||
| async def _on_list_tools( | ||
@@ -143,9 +160,14 @@ _context: ServerRequestContext[dict[str, Any]], | ||
| ) -> types.ListToolsResult: | ||
| """Adapter: the router's wire shapes into pydantic. | ||
| Generic on purpose — register another tool above and this does not change. | ||
| """ | ||
| return types.ListToolsResult( | ||
| tools=[ | ||
| types.Tool( | ||
| name=TOOLS[0]["name"], | ||
| description=TOOLS[0]["description"], | ||
| input_schema=ECHO_INPUT_SCHEMA, | ||
| name=tool["name"], | ||
| description=tool["description"], | ||
| input_schema=tool["inputSchema"], | ||
| ) | ||
| for tool in ROUTER.list_tools() | ||
| ] | ||
@@ -159,10 +181,18 @@ ) | ||
| ) -> types.CallToolResult: | ||
| if params.name != TOOLS[0]["name"]: | ||
| return _error_result(f"unknown tool {params.name}") | ||
| text = (params.arguments or {}).get("text") | ||
| if not isinstance(text, str): | ||
| return _error_result("text must be a string") | ||
| return _json_result(echo(text)) | ||
| """The other adapter. | ||
| ``call_tool`` never raises for a bad call — an unknown tool, a failed validation and | ||
| a handler exception all come back as an error result — so there is no error handling | ||
| here to get wrong. | ||
| """ | ||
| result = await ROUTER.call_tool(params.name, params.arguments) | ||
| return types.CallToolResult( | ||
| content=[ | ||
| types.TextContent(type="text", text=block["text"]) | ||
| for block in result["content"] | ||
| ], | ||
| is_error=result.get("isError", False), | ||
| ) | ||
| def create_server() -> Server[dict[str, Any]]: | ||
@@ -169,0 +199,0 @@ return Server( |
67326
1.59%1254
1.87%