feat: add MCP Runtime Server Plugin (plugin-mcp-server)#1108
Conversation
Implements the MCP Runtime Server plugin that bridges ObjectStack kernel services to the Model Context Protocol (MCP). Features: - Tool bridge: all ToolRegistry tools → MCP tools - Resource bridge: MetadataService → object schemas, DataEngine → records - Prompt bridge: Agent definitions → MCP prompts - StdioServerTransport for local AI client integration - Environment variable configuration (MCP_SERVER_ENABLED, MCP_SERVER_NAME) Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/5eb90a65-846f-4f24-947f-1c0e7ea5090e Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
25 tests covering: - MCPServerRuntime: constructor, bridgeTools, bridgeResources, bridgePrompts, lifecycle - MCPServerPlugin: metadata, init, start (with/without services), destroy - Graceful degradation when services are missing Also updates CHANGELOG.md with new MCP Runtime Server Plugin feature. Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/5eb90a65-846f-4f24-947f-1c0e7ea5090e Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…mas, fix type safety - Extract formatToolOutput() static helper for output transformation - Replace `as any` casts in tool handler with explicit Record<string,unknown> - Use Zod schemas for prompt argsSchema (z.string().describe() vs raw objects) - Add zod@^4.3.6 dependency aligned with @objectstack/spec - Document duck-typing pattern for IAIService.toolRegistry access Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/5eb90a65-846f-4f24-947f-1c0e7ea5090e Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Moves READ_ONLY_TOOLS and DESTRUCTIVE_TOOLS from inline arrays to module-level Set constants for better maintainability. Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/5eb90a65-846f-4f24-947f-1c0e7ea5090e Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
Adds a new kernel plugin package (@objectstack/plugin-mcp-server) that exposes ObjectStack services (AI tools, metadata resources, and agent prompts) via the Model Context Protocol (MCP) for consumption by external AI clients over a stdio-based MCP server runtime.
Changes:
- Introduces
plugin-mcp-serverpackage (runtime + kernel plugin) bridging ToolRegistry → MCP tools, Metadata/DataEngine → MCP resources, and Agents → MCP prompts. - Adds Vitest coverage for plugin lifecycle and runtime bridging behavior.
- Updates lockfile and changelog to include the new plugin and MCP SDK dependency.
Reviewed changes
Copilot reviewed 9 out of 10 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Adds @modelcontextprotocol/sdk and transitive deps for the new plugin package. |
| packages/plugins/plugin-mcp-server/package.json | Defines the new plugin package, build/test scripts, and dependencies. |
| packages/plugins/plugin-mcp-server/tsconfig.json | TypeScript build config for the new plugin package. |
| packages/plugins/plugin-mcp-server/src/types.ts | Duck-typed ToolRegistry + ToolExecutionResult interface used by the MCP bridge. |
| packages/plugins/plugin-mcp-server/src/plugin.ts | Kernel plugin lifecycle: init/register service, bridge services on start, optional auto-start, trigger mcp:ready. |
| packages/plugins/plugin-mcp-server/src/mcp-server-runtime.ts | MCP runtime: registers tools/resources/prompts and manages stdio transport lifecycle. |
| packages/plugins/plugin-mcp-server/src/index.ts | Public exports for the plugin and runtime. |
| packages/plugins/plugin-mcp-server/src/tests/plugin.test.ts | Tests plugin lifecycle behavior and graceful degradation when services are missing. |
| packages/plugins/plugin-mcp-server/src/tests/mcp-server-runtime.test.ts | Tests tool/resource/prompt bridging and lifecycle warnings. |
| CHANGELOG.md | Documents the new MCP plugin and its capabilities under Unreleased. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
| const config: MCPServerRuntimeConfig = { | ||
| name: process.env.MCP_SERVER_NAME ?? this.options.name ?? 'objectstack', | ||
| version: this.options.version ?? '1.0.0', | ||
| transport: (process.env.MCP_SERVER_TRANSPORT as 'stdio' | 'http') ?? this.options.transport ?? 'stdio', | ||
| instructions: this.options.instructions, | ||
| logger: ctx.logger, | ||
| }; | ||
|
|
||
| this.runtime = new MCPServerRuntime(config); | ||
| ctx.registerService('mcp', this.runtime); | ||
|
|
||
| ctx.logger.info('[MCP] Plugin initialized'); |
There was a problem hiding this comment.
When using MCP stdio transport, the protocol stream is written over process.stdout. Passing ctx.logger into the runtime and logging during startup will also write to stdout by default (ObjectLogger’s pino transport uses destination 1), which can corrupt the MCP message stream for clients. Consider routing MCP-related logs to stderr/a file, or disabling stdout logging while the stdio transport is connected.
| version: this.options.version ?? '1.0.0', | ||
| transport: (process.env.MCP_SERVER_TRANSPORT as 'stdio' | 'http') ?? this.options.transport ?? 'stdio', | ||
| instructions: this.options.instructions, |
There was a problem hiding this comment.
MCP_SERVER_TRANSPORT is cast to 'stdio' | 'http' without validation. If the env var contains any other value, it will still be used at runtime and start() will fall into the non-stdio branch (with an HTTP-specific warning that may be misleading). Please validate/normalize the env var to the allowed set and fall back to the default when invalid.
| // Convert JSON Schema parameters to Zod-compatible format for MCP SDK | ||
| // The MCP SDK registerTool with inputSchema expects a Zod raw shape or AnySchema. | ||
| // Since our tools use JSON Schema, we use the low-level .tool() with a raw callback | ||
| // and pass the JSON Schema as annotations metadata. | ||
| this.mcpServer.registerTool( | ||
| tool.name, | ||
| { | ||
| description: tool.description, | ||
| annotations: { | ||
| // Mark tools with write side-effects for destructive operations | ||
| destructiveHint: this.isDestructiveTool(tool.name), | ||
| readOnlyHint: this.isReadOnlyTool(tool.name), | ||
| openWorldHint: false, | ||
| }, | ||
| }, |
There was a problem hiding this comment.
Tool parameter schemas from AIToolDefinition.parameters are currently ignored when registering MCP tools. That means MCP clients can’t discover/validate the expected arguments, which also contradicts the inline comment about passing JSON Schema metadata. Consider converting the JSON Schema to an MCP-compatible schema (or at least attaching the JSON Schema in a documented metadata field) so clients can generate correct tool UIs and calls.
| const result = await toolRegistry.execute({ | ||
| type: 'tool-call', | ||
| toolCallId: `mcp-${tool.name}-${Date.now()}`, | ||
| toolName: tool.name, | ||
| input: args, | ||
| }); |
There was a problem hiding this comment.
toolCallId is generated with Date.now(), which can collide under high-frequency or parallel tool calls and makes debugging/replay harder. Prefer a UUID (e.g., crypto.randomUUID()) or a monotonic counter scoped to the runtime instance to guarantee uniqueness.
| async (_uri, variables) => { | ||
| const objectName = String(variables.objectName); | ||
| const recordId = String(variables.recordId); | ||
|
|
||
| try { | ||
| const record = await dataEngine.findOne(objectName, { | ||
| where: { id: recordId }, | ||
| }); |
There was a problem hiding this comment.
The record resource (objectstack://objects/{objectName}/records/{recordId}) exposes raw records via IDataEngine.findOne() with no auth/user context. Since IDataEngine doesn’t take a request principal, this can effectively run with system-level access and leak sensitive fields when MCP is enabled. Consider requiring an explicit opt-in/allowlist for record access, or integrating with the platform’s security/auth layer before returning record data.
Bridges all ObjectStack kernel services to the Model Context Protocol so external AI clients (Claude Desktop, Cursor, VS Code Copilot, etc.) can consume platform tools, data, and agent prompts via MCP's standardized interface.
Tool Bridge
All
ToolRegistrytools (9 built-in:list_objects,describe_object,query_records,get_record,aggregate_data,create_object,add_field,modify_field,delete_field) automatically exposed as MCP tools withreadOnlyHint/destructiveHintannotations.Resource Bridge
Four MCP resource endpoints:
objectstack://objects— object listobjectstack://objects/{objectName}— object schema viaResourceTemplateobjectstack://objects/{objectName}/records/{recordId}— record by IDobjectstack://metadata/types— registered metadata typesPrompt Bridge
agent_promptMCP prompt with Zod-typed args (agentName,objectName,recordId,viewName) loads agent instructions + UI context dynamically fromIMetadataService.Plugin Lifecycle
init: createsMCPServerRuntime, registers as'mcp'servicestart: auto-detectsai,metadata,dataservices; gracefully degrades when missing; firesmcp:readyhookdestroy: stops transportConfiguration
Studio frontend unaffected
Studio AI continues using REST/SSE via Vercel Data Stream Protocol. MCP is a separate channel for external clients.
Dependencies
@modelcontextprotocol/sdk@^1.29.0(no advisories)zod@^4.3.6(aligned with@objectstack/spec)@objectstack/service-aivia duck-typedToolRegistryinterface intypes.ts