mirror of
https://github.com/Sea-Haven-Industries/sh-mcp.git
synced 2026-10-02 16:33:18 +00:00
103 lines
3.7 KiB
TypeScript
103 lines
3.7 KiB
TypeScript
|
|
/**
|
||
|
|
* MCP (Model Context Protocol) transport over the shared registry.
|
||
|
|
*
|
||
|
|
* Builds a low-level `@modelcontextprotocol/sdk` `Server` per authenticated
|
||
|
|
* request, bound to the caller's `AuthContext`, exposing exactly two handlers:
|
||
|
|
*
|
||
|
|
* - `tools/list` returns ONLY the tools whose `requiredScope` is in the
|
||
|
|
* caller's scopes — server-side TOOL-HIDING (design.md §2.5). A finance-less
|
||
|
|
* caller never sees finance tools.
|
||
|
|
* - `tools/call` routes through {@link executeTool}, so scope enforcement,
|
||
|
|
* input validation, rate limiting, finance redaction, and audit are
|
||
|
|
* identical to the OpenAPI path. Hiding is convenience; the 403 from
|
||
|
|
* `executeTool` is the real boundary (a forced call to a hidden tool still
|
||
|
|
* fails).
|
||
|
|
*
|
||
|
|
* The low-level `Server` (not the Zod-based `McpServer`) is used deliberately:
|
||
|
|
* our tools carry JSON-Schema input schemas and we need per-caller dynamic tool
|
||
|
|
* lists, which the high-level helper does not support cleanly.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||
|
|
import {
|
||
|
|
CallToolRequestSchema,
|
||
|
|
ListToolsRequestSchema,
|
||
|
|
ErrorCode,
|
||
|
|
McpError,
|
||
|
|
} from '@modelcontextprotocol/sdk/types.js';
|
||
|
|
|
||
|
|
import { executeTool, InputValidationError, UnknownToolError } from './dispatch.js';
|
||
|
|
import { ScopeError } from './auth.js';
|
||
|
|
import { RateLimitError } from './rate-limit.js';
|
||
|
|
import { visibleTools } from './visibility.js';
|
||
|
|
import type { DispatchDeps } from './dispatch.js';
|
||
|
|
import type { ToolRegistry } from './registry.js';
|
||
|
|
import type { AuthContext } from './types.js';
|
||
|
|
|
||
|
|
/** Server identity advertised in the MCP handshake. */
|
||
|
|
export interface McpServerInfo {
|
||
|
|
name: string;
|
||
|
|
version: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create a low-level MCP `Server` scoped to one authenticated caller.
|
||
|
|
*
|
||
|
|
* @param registry the server's tool registry
|
||
|
|
* @param ctx the authenticated caller (drives tool-hiding)
|
||
|
|
* @param deps audit + rate-limit dependencies for the dispatch path
|
||
|
|
* @param info MCP server identity for the handshake
|
||
|
|
*/
|
||
|
|
export function createMcpServer(
|
||
|
|
registry: ToolRegistry,
|
||
|
|
ctx: AuthContext,
|
||
|
|
deps: DispatchDeps,
|
||
|
|
info: McpServerInfo,
|
||
|
|
): Server {
|
||
|
|
const server = new Server(info, { capabilities: { tools: {} } });
|
||
|
|
|
||
|
|
// tools/list — scope-filtered (tool-hiding).
|
||
|
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||
|
|
tools: visibleTools(registry, ctx).map((tool) => ({
|
||
|
|
name: tool.name,
|
||
|
|
description: tool.description,
|
||
|
|
inputSchema: tool.inputSchema as { type: 'object' },
|
||
|
|
})),
|
||
|
|
}));
|
||
|
|
|
||
|
|
// tools/call — single authoritative dispatch path.
|
||
|
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||
|
|
const { name, arguments: args } = request.params;
|
||
|
|
try {
|
||
|
|
const output = await executeTool(registry, ctx, name, args ?? {}, deps);
|
||
|
|
return {
|
||
|
|
content: [{ type: 'text', text: JSON.stringify(output) }],
|
||
|
|
structuredContent: output as Record<string, unknown>,
|
||
|
|
};
|
||
|
|
} catch (err) {
|
||
|
|
throw toMcpError(err);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
return server;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Map dispatch errors to MCP protocol errors (no secrets / stack traces). */
|
||
|
|
function toMcpError(err: unknown): McpError {
|
||
|
|
if (err instanceof UnknownToolError) {
|
||
|
|
return new McpError(ErrorCode.MethodNotFound, err.message);
|
||
|
|
}
|
||
|
|
if (err instanceof ScopeError) {
|
||
|
|
return new McpError(ErrorCode.InvalidRequest, err.message);
|
||
|
|
}
|
||
|
|
if (err instanceof InputValidationError) {
|
||
|
|
return new McpError(ErrorCode.InvalidParams, err.message);
|
||
|
|
}
|
||
|
|
if (err instanceof RateLimitError) {
|
||
|
|
return new McpError(ErrorCode.InvalidRequest, err.message);
|
||
|
|
}
|
||
|
|
if (err instanceof McpError) return err;
|
||
|
|
// Generic handler failure: do not leak the underlying message verbatim.
|
||
|
|
return new McpError(ErrorCode.InternalError, 'Tool execution failed.');
|
||
|
|
}
|