diff --git a/.changeset/mcp-readme-shipped-surface.md b/.changeset/mcp-readme-shipped-surface.md new file mode 100644 index 0000000000..4819ea0198 --- /dev/null +++ b/.changeset/mcp-readme-shipped-surface.md @@ -0,0 +1,68 @@ +--- +"@objectstack/mcp": patch +--- + +docs(mcp): rewrite the published README to the shipped host-extension surface (#9579) + +`packages/mcp/README.md` is in the package's `files` array with `private` unset, +so it is the page npm renders. It told the reader to extend the server +imperatively at six call sites: + +```ts +kernel.getService('mcp').registerTool(calculateRevenueTool); +kernel.getService('mcp').registerResource({ … }); +kernel.getService('mcp').registerPrompt({ … }); +``` + +`MCPServerRuntime` has never had any of those members. Measured against the +built `dist/index.d.ts`, a consumer who copies those lines gets three +`TS2339 Property … does not exist on type 'MCPServerRuntime'`. The receiver is a +local variable, so `check:published-readme-exports` is structurally blind to +them — both of its halves key on a name the fence *imported*, and this one is +neither imported nor a bare identifier. + +Ruled 2026-08-18: **document the shipped surface; do not grow the API to match +the docs.** So the imperative narrative is gone and the page now documents what +actually ships — the bridge methods (`bridgeTools`, `bridgeDataTools`, +`bridgeResources`, `bridgePrompts`), `handleHttpRequest` / `renderSkill`, and the +exported `registerObjectTools` / `registerActionTools` / `registerSkillPrompts` +helpers driving an `McpServer`. Every row is probed against the built type entry +the `exports` map resolves, and the page's one host-extension example compiles +clean against it. + +Neighbouring fabrications the audit turned up, all corrected in the same pass — +each of them was reachable only through prose or an unimported receiver, which is +why nothing had read them: + +- **A tool family that does not exist.** The page listed + `objectstack_find` / `objectstack_findOne` / `objectstack_create` / + `objectstack_update` / `objectstack_delete` / `objectstack_describeObject` / + `objectstack_listObjects` / `objectstack_listFields` as "auto-registered". No + such tool name occurs anywhere in the repo. The real names are the + `list_objects` … `run_action` set the page listed separately, one section down. +- **`aggregate_records` was missing** from the list that *was* correct, along + with the fact that it registers only when the bridge implements `aggregate`. +- **Resource URIs were wrong in both directions.** The page taught + `objectstack://objects/{name}/records` (no such resource) and + `objectstack://objects/{name}/{id}` (real shape is + `…/{name}/records/{id}`), and omitted `objectstack://objects` and + `objectstack://metadata/types` entirely. +- **The advertised capability block was invented.** It claimed + `tools.listChanged`, `resources.subscribe`, `resources.listChanged`, + `prompts.listChanged` and `experimental.streaming`. The server hand-declares + only `logging`; everything else is *derived* from what was actually registered, + which is the ADR-0076 D12 contract the README was contradicting. The + "Streaming Support" feature bullet and the streaming-resource example went with + it — neither names anything that ships. +- **The stdio transport could not be started by following the page.** Neither + `OS_MCP_STDIO_ENABLED` nor `OS_MCP_STDIO_API_KEY` was documented, and stdio + auto-start refuses to boot without the key (ADR-0101, fail-closed). The three + client config blocks now carry both. The Debugging section also taught + `OS_MCP_SERVER_ENABLED=true` as the stdio switch, which is the deprecated path + that logs a warning. +- **A broken relative link.** `../../spec/src/ai/` resolves above the repo root + from `packages/mcp/`; the target is `../spec/src/ai/`. + +Docs only — no runtime code changed, and no API was added. `registerTool` / +`registerResource` / `registerPrompt` remain unbuilt by ruling; a future +imperative API is its own card on measured pull. diff --git a/packages/mcp/README.md b/packages/mcp/README.md index 343ac1b549..cfd4442eb5 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -1,17 +1,16 @@ # @objectstack/mcp -MCP Runtime Server Plugin for ObjectStack — exposes AI tools, data resources, and agent prompts via the Model Context Protocol. +MCP Runtime Server Plugin for ObjectStack — exposes your app's data objects, business actions, and authored skills over the Model Context Protocol (stdio + Streamable HTTP). ## Features -- **Model Context Protocol (MCP)**: Expose ObjectStack resources to AI models via MCP -- **AI Tools**: Auto-generate MCP tools from ObjectStack actions and flows -- **Data Resources**: Expose objects, records, and metadata as MCP resources -- **Agent Prompts**: Register prompt templates for AI agents -- **Type-Safe**: Full Zod schema validation for tool inputs/outputs -- **Auto-Discovery**: MCP clients automatically discover available tools and resources -- **Streaming Support**: Stream large datasets and real-time updates -- **Security**: Built-in permission checks for tool execution +- **Model Context Protocol (MCP)**: expose an ObjectStack app to any MCP client +- **Object tools**: list / describe / query / aggregate / read / create / update / delete, generated from your metadata +- **Action tools**: invoke the business actions an author opted into the AI surface +- **Resources**: object schemas, records and metadata types as MCP resources +- **Prompts**: authored skills and registered agents project as MCP prompts +- **Two transports**: a long-lived stdio server, and a per-request Streamable HTTP endpoint at `/api/v1/mcp` +- **Security**: every call runs under a real principal — permissions, row-level and field-level security apply, and OAuth scopes narrow the tool families ## What is MCP? @@ -73,51 +72,65 @@ AI tool registry, metadata service and data engine automatically when it starts. The HTTP surface needs no start at all: it is served per-request at `/api/v1/mcp` (default-on; `OS_MCP_SERVER_ENABLED=false` opts out). -Environment overrides: `OS_MCP_SERVER_NAME`, `OS_MCP_SERVER_TRANSPORT`, -`OS_MCP_SERVER_ENABLED`. +### Environment variables -## MCP Tools +| Variable | Effect | +|---|---| +| `OS_MCP_SERVER_ENABLED` | HTTP surface, **default-on**. `false` disables it. | +| `OS_MCP_SERVER_NAME` | Override the server name. | +| `OS_MCP_SERVER_TRANSPORT` | Override the transport (`stdio` \| `http`). | +| `OS_MCP_STDIO_ENABLED` | Auto-start the **long-lived stdio** transport (equivalent to the `autoStart` option). | +| `OS_MCP_STDIO_API_KEY` | The identity the stdio server runs as. **Required** whenever stdio auto-starts. | -### Auto-Generated Tools +The stdio transport has its own switch on purpose: starting it claims the +process's stdin/stdout. Setting `OS_MCP_SERVER_ENABLED=true` also starts stdio, +but that path is **deprecated** and logs a warning — use `OS_MCP_STDIO_ENABLED` +or the `autoStart` option. -ObjectStack automatically exposes these operations as MCP tools: +`OS_MCP_STDIO_API_KEY` is not optional and has no fallback: a stdio server with +no resolvable principal **refuses to start** rather than serving data unscoped +(ADR-0101). Mint a key in Setup → Connect an Agent, or `POST /api/v1/keys`. -```typescript -// CRUD operations (auto-registered) -'objectstack_find' // Query records -'objectstack_findOne' // Get single record -'objectstack_create' // Create record -'objectstack_update' // Update record -'objectstack_delete' // Delete record - -// Metadata operations -'objectstack_describeObject' // Get object schema -'objectstack_listObjects' // List all objects -'objectstack_listFields' // List object fields -``` +The legacy `MCP_SERVER_*` spellings are still honoured with a deprecation +warning. + +## The tool surface -### Native Tools (Streamable HTTP) +There are two independent families, and which one you get depends on how the +server is assembled. -Over the network-reachable Streamable HTTP transport, the server self-registers -a native tool set bound to the **caller's principal** (the API key acts as the -user, with full row-level security + permission enforcement). No -`@objectstack/service-ai` and no cloud studio are required — these are part of -the open framework. +### Object and action tools + +Registered from a **data bridge** the host supplies — the surface both +transports serve. Every call runs as the caller (permissions, RLS and FLS +apply): ```typescript -// Object data (RLS-enforced as the caller) -'list_objects' // List objects (system sys_* objects hidden by default) -'describe_object' // Object schema: fields + features -'validate_expression' // Check a CEL expression against a schema before authoring it -'query_records' // Filter / sort / paginate -'get_record' // Fetch one by id -'create_record' / 'update_record' / 'delete_record' +// Object data +'list_objects' // List objects (system sys_* objects hidden by default) +'describe_object' // Object schema: fields + features +'validate_expression' // Check a CEL expression against a schema before authoring it +'query_records' // Filter / sort / paginate +'aggregate_records' // count/sum/avg/min/max/count_distinct, optionally grouped +'get_record' // Fetch one by id +'create_record' // Create +'update_record' // Update by id +'delete_record' // Delete by id (destructive) // Business actions — operate the app, not just its rows -'list_actions' // Invokable business actions the caller may run -'run_action' // Invoke an action by name with { recordId, params } +'list_actions' // Invokable business actions the caller may run +'run_action' // Invoke an action by name with { recordId, params } ``` +`aggregate_records` is registered only when the bridge implements `aggregate`; +a bridge without that seam serves the rest and advertises nothing it cannot do. + +OAuth scopes narrow the families at consent time: `data:read` covers +list/describe/query/aggregate/get, `data:write` covers create/update/delete, and +`actions:execute` covers `list_actions` / `run_action`. A tool outside the grant +is **not registered at all**, so the SDK rejects it as an unknown tool — the +grant doubles as dispatch-time enforcement. + `list_actions` enumerates each object's headless-invokable actions (script / flow), filtered to what the author exposed and the caller may run: only actions opted into the AI surface (`ai: { exposed: true }`, ADR-0011 / #2849) are @@ -136,152 +149,114 @@ task", "convert this lead". > behalf of anyone allowed through the gate. Flow actions honour the flow's > `runAs` declaration (ADR-0049) with the caller's identity forwarded. -### Custom Tools +### AI-registry tools -Register custom tools that AI models can call: +If the deployment also runs an AI service that exposes a function-calling +`ToolRegistry`, the plugin bridges every tool in it onto the long-lived server +under the same name, description and JSON Schema. This family is empty on an app +that registers no AI tools, and its absence is reported honestly: no tools +registered means the `tools` capability is not advertised. -```typescript -import { defineTool } from '@objectstack/spec'; - -const calculateRevenueTool = defineTool({ - name: 'calculate_revenue', - description: 'Calculate total revenue for an account', - inputSchema: { - type: 'object', - properties: { - accountId: { type: 'string', description: 'Account ID' }, - startDate: { type: 'string', description: 'Start date (ISO 8601)' }, - endDate: { type: 'string', description: 'End date (ISO 8601)' }, - }, - required: ['accountId'], - }, - async execute({ accountId, startDate, endDate }) { - const opportunities = await kernel.getDriver().find({ - object: 'opportunity', - filters: [ - { field: 'account_id', operator: 'eq', value: accountId }, - { field: 'stage', operator: 'eq', value: 'closed_won' }, - { field: 'close_date', operator: 'gte', value: startDate }, - { field: 'close_date', operator: 'lte', value: endDate }, - ], - }); - - const total = opportunities.reduce((sum, opp) => sum + opp.amount, 0); - - return { - accountId, - totalRevenue: total, - opportunityCount: opportunities.length, - }; - }, -}); +## Resources + +Registered by the resource bridge, from your metadata: -// Register with MCP server -kernel.getService('mcp').registerTool(calculateRevenueTool); +``` +objectstack://objects # List all data objects +objectstack://objects/{objectName} # Object schema +objectstack://objects/{objectName}/records/{recordId} # One record +objectstack://metadata/types # List all metadata types ``` -## MCP Resources +The record resource is registered only when the host supplies a record reader; +without one, the schema and listing resources are served alone. -### Auto-Exposed Objects +## Prompts -All ObjectStack objects are automatically exposed as MCP resources: +Two prompt families are bridged — there is no per-item prompt registration call: -``` -objectstack://objects/opportunity # Opportunity object schema -objectstack://objects/opportunity/records # All opportunity records -objectstack://objects/opportunity/123 # Specific opportunity record -``` +1. **`agent_prompt`** — one dynamic prompt that loads a registered agent's + system prompt by name, with optional UI context (`objectName`, `recordId`, + `viewName`). +2. **One prompt per authored skill** that carries `instructions` (#3905). Write + a `*.skill.ts`, and its instructions become a prompt any connected MCP client + can list and fetch. -### Custom Resources +The skill **list** is a snapshot taken when the bridge runs; each prompt's +**body** is re-read from metadata at `prompts/get` time, so an edited skill +serves fresh text without a restart. -Expose custom resources to AI models: +## Extending the server from a host -```typescript -kernel.getService('mcp').registerResource({ - uri: 'objectstack://reports/sales-pipeline', - name: 'Sales Pipeline Report', - description: 'Current sales pipeline with stages and amounts', - mimeType: 'application/json', - async read() { - const opportunities = await kernel.getDriver().find({ - object: 'opportunity', - filters: [ - { field: 'stage', operator: 'neq', value: 'closed_won' }, - { field: 'stage', operator: 'neq', value: 'closed_lost' }, - ], - }); - - const pipeline = opportunities.reduce((acc, opp) => { - acc[opp.stage] = (acc[opp.stage] || 0) + opp.amount; - return acc; - }, {}); - - return { - content: [ - { - type: 'text', - text: JSON.stringify(pipeline, null, 2), - }, - ], - }; - }, -}); -``` +⚠️ There is **no** imperative `registerTool()` / `registerResource()` / +`registerPrompt()` call on the `'mcp'` service. Tools, resources and prompts are +derived from metadata, and a host that drives the runtime itself contributes +them through the bridge methods and the exported helpers below. + +`MCPServerRuntime`'s public surface, as published in `dist/index.d.ts`: -## MCP Prompts +| Member | What it does | +|---|---| +| `new MCPServerRuntime(config?)` | `MCPServerRuntimeConfig`: `name`, `version`, `instructions`, `transport`, `logger`. | +| `server` | The underlying `McpServer` (getter), for advanced use. | +| `isStarted` | Whether a transport is currently connected (getter). | +| `bridgeTools(toolRegistry)` | Bridge an AI service's function-calling `ToolRegistry`. | +| `bridgeDataTools(bridge, toolOptions?)` | Register the object-CRUD tools, plus the action pair when the bridge carries that seam. Returns the tool names registered. | +| `bridgeResources(metadataService, getRecord?)` | Register the `objectstack://` resources. | +| `bridgePrompts(metadataService, mergedRead?)` | Register the agent and skill prompts. | +| `start()` / `stop()` | Attach / detach the configured long-lived transport. | +| `renderSkill(options?)` | Render the portable Agent Skill (`SKILL.md`) for this environment. | +| `handleHttpRequest(request, opts?)` | Serve one Streamable HTTP request (Web-standard `Request`/`Response`). | -Register prompt templates that AI models can use: +Ordering matters: **bridge everything before `start()`**. Registering a tool, +resource or prompt is also what declares its capability, and the MCP SDK refuses +to register capabilities once a transport is attached. + +The three helpers the bridges use are exported so a host can drive an +`McpServer` directly: ```typescript -kernel.getService('mcp').registerPrompt({ - name: 'analyze_account', - description: 'Analyze an account and its opportunities', - arguments: [ - { - name: 'accountId', - description: 'Account ID to analyze', - required: true, - }, - ], - async render({ accountId }) { - const account = await kernel.getDriver().findOne({ - object: 'account', - filters: [{ field: 'id', operator: 'eq', value: accountId }], - }); - - const opportunities = await kernel.getDriver().find({ - object: 'opportunity', - filters: [{ field: 'account_id', operator: 'eq', value: accountId }], - }); - - return { - messages: [ - { - role: 'user', - content: { - type: 'text', - text: `Analyze this account and provide insights: - -Account: ${account.name} -Industry: ${account.industry} -Total Opportunities: ${opportunities.length} -Total Value: $${opportunities.reduce((sum, o) => sum + o.amount, 0)} - -Opportunities: -${opportunities.map(o => `- ${o.name} (${o.stage}): $${o.amount}`).join('\n')} - -Please provide: -1. Key insights about this account -2. Risk assessment -3. Recommendations for next steps`, - }, - }, - ], - }; - }, +import { + MCPServerRuntime, + registerObjectTools, + registerActionTools, + registerSkillPrompts, +} from '@objectstack/mcp'; +import type { McpDataBridge, McpActionBridge, McpSkillBridge } from '@objectstack/mcp'; + +// Your host supplies these, bound to the caller's principal. +declare const data: McpDataBridge & McpActionBridge; +declare const skills: McpSkillBridge; + +const runtime = new MCPServerRuntime({ + name: 'my-host', + version: '1.0.0', + transport: 'stdio', }); + +// Either: one call for the whole data surface, returning the names registered. +const registered: string[] = runtime.bridgeDataTools(data, { maxQueryLimit: 200 }); + +// Or: drive the helpers against the underlying McpServer yourself. +registerObjectTools(runtime.server, data, { allowSystemObjects: false }); +registerActionTools(runtime.server, data, { grantedScopes: ['actions:execute'] }); +registerSkillPrompts(runtime.server, skills); + +await runtime.start(); ``` +`RegisterObjectToolsOptions` carries `allowSystemObjects`, `maxQueryLimit` and +`grantedScopes`; `RegisterActionToolsOptions` carries `allowSystemObjects` and +`grantedScopes`. `McpDataBridge` is the data seam (`listObjects`, +`describeObject`, `query`, `get`, `create`, `update`, `remove`, and the optional +`aggregate` / `listObjectsDiagnosed`); `McpActionBridge` adds `listActions` and +`runAction`; `McpSkillBridge` is a single `listSkills`. + +Also exported for hosts that render the skill surface themselves: +`renderSkillMarkdown`, `listSkillPrompts`, `projectSkillPrompt`, +`skillPromptResult`, `OBJECTSTACK_SKILL_NAME`, `OBJECTSTACK_SKILL_DESCRIPTION`, +and the Setup page metadata `CONNECT_AGENT_PAGE` / `CONNECT_AGENT_UI_BUNDLE`. + ## Using with AI Clients ### Connecting to a running deployment (remote HTTP) @@ -341,7 +316,9 @@ are also accepted.) ### Claude Desktop (local stdio server) -Add to `~/Library/Application Support/Claude/claude_desktop_config.json`: +Add to `~/Library/Application Support/Claude/claude_desktop_config.json`. The +stdio transport needs both of its switches — the enable flag and the identity it +runs as: ```json { @@ -350,7 +327,9 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`: "command": "node", "args": ["/path/to/your/objectstack/server.js"], "env": { - "DATABASE_URL": "your-database-url" + "DATABASE_URL": "your-database-url", + "OS_MCP_STDIO_ENABLED": "true", + "OS_MCP_STDIO_API_KEY": "osk_..." } } } @@ -359,14 +338,18 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`: ### Cursor IDE -Add to `.cursor/mcp.json`: +Add to `.cursor/mcp.json` (same two environment variables apply): ```json { "mcpServers": { "objectstack": { "command": "node", - "args": ["./server.js"] + "args": ["./server.js"], + "env": { + "OS_MCP_STDIO_ENABLED": "true", + "OS_MCP_STDIO_API_KEY": "osk_..." + } } } } @@ -381,7 +364,11 @@ Configure in Cline settings: "cline.mcpServers": { "objectstack": { "command": "node", - "args": ["./server.js"] + "args": ["./server.js"], + "env": { + "OS_MCP_STDIO_ENABLED": "true", + "OS_MCP_STDIO_API_KEY": "osk_..." + } } } } @@ -389,7 +376,7 @@ Configure in Cline settings: ## Server Implementation -### Stdio Transport (Default) +### Stdio Transport ```typescript // objectstack.config.ts @@ -428,9 +415,10 @@ export default defineStack({ ``` Run it with the CLI (`os dev` / `os serve`) — `defineStack()` returns the -metadata definition; the CLI boots the kernel from it. +metadata definition; the CLI boots the kernel from it. With `autoStart: true` +you must also set `OS_MCP_STDIO_API_KEY`, or boot fails closed. -### HTTP Transport +### HTTP Transport (default) ```typescript export default defineStack({ @@ -445,134 +433,49 @@ export default defineStack({ // Served per-request by the running server at /api/v1/mcp ``` -## Advanced Features - -### Streaming Resources - -```typescript -kernel.getService('mcp').registerResource({ - uri: 'objectstack://exports/opportunities-csv', - name: 'Opportunities Export (CSV)', - mimeType: 'text/csv', - async *stream() { - // Stream header - yield 'Name,Stage,Amount,Close Date\n'; - - // Stream records in batches - let offset = 0; - const batchSize = 100; - - while (true) { - const batch = await kernel.getDriver().find({ - object: 'opportunity', - limit: batchSize, - offset, - }); - - if (batch.length === 0) break; - - for (const opp of batch) { - yield `${opp.name},${opp.stage},${opp.amount},${opp.close_date}\n`; - } - - offset += batchSize; - } - }, -}); -``` - -### Tool Permissions - -```typescript -kernel.getService('mcp').registerTool({ - name: 'delete_opportunity', - description: 'Delete an opportunity', - permissions: ['opportunity:delete'], // Require permission - inputSchema: { - type: 'object', - properties: { - id: { type: 'string' }, - }, - required: ['id'], - }, - async execute({ id }, context) { - // context includes userId, permissions, etc. - if (!context.hasPermission('opportunity:delete')) { - throw new Error('Permission denied'); - } - - await kernel.getDriver().delete({ - object: 'opportunity', - filters: [{ field: 'id', operator: 'eq', value: id }], - }); - - return { success: true, deleted: id }; - }, -}); -``` - -### Dynamic Tool Registration - -```typescript -// Register tools from flow definitions -const flows = await kernel.getMetadata('flow'); - -for (const flow of flows) { - kernel.getService('mcp').registerTool({ - name: `flow_${flow.name}`, - description: flow.description, - inputSchema: generateSchemaFromFlow(flow), - async execute(inputs) { - return await kernel.executeFlow(flow.name, inputs); - }, - }); -} -``` - -## Server Capabilities - -The MCP server exposes these capabilities: - -```json -{ - "capabilities": { - "tools": { - "listChanged": true - }, - "resources": { - "subscribe": true, - "listChanged": true - }, - "prompts": { - "listChanged": true - }, - "logging": {}, - "experimental": { - "streaming": true - } - } -} -``` - -## Best Practices - -1. **Tool Design**: Keep tools focused and well-documented -2. **Resource Naming**: Use clear, hierarchical URI schemes -3. **Prompt Templates**: Make prompts flexible with arguments -4. **Error Handling**: Always return helpful error messages -5. **Permissions**: Check permissions before tool execution -6. **Performance**: Use streaming for large datasets -7. **Versioning**: Version your server and tools +## Server capabilities + +The server does **not** hand-declare a capability set. Capabilities are derived +from what was actually registered, because the SDK's registration call installs +the handler and declares the capability together — so there is no way to +advertise a primitive this server cannot serve (ADR-0076 D12). + +In practice: `tools` appears once a tool is registered, `resources` once a +resource is, `prompts` once the skill/agent bridge runs. `logging` is the one +hand-declared entry, and it is honest — declaring it is itself what wires the +`logging/setLevel` handler. + +There is no `subscribe`, no `listChanged` and no streaming capability. An app +with no bridged tools advertises no `tools` capability, which is the honest +report rather than an empty promise. + +## Best practices + +1. **Model the app, not the transport** — tools come from your objects and + actions, so the lever that shapes the AI surface is your metadata. +2. **Opt actions in deliberately** — `ai: { exposed: true }` is a security + decision; an invoked action body runs as trusted app code. +3. **Scope the grant** — hand `grantedScopes` the narrowest set the client + needs; an ungranted tool is never registered. +4. **Cap the reads** — `maxQueryLimit` bounds what one `query_records` call can + pull. +5. **Prefer `aggregate_records` over paging** — a question about totals should + not walk every row. +6. **Bridge before `start()`** — capabilities cannot be declared once a + transport is attached. ## Debugging The plugin logs through the kernel logger — there is no `debug` option. The -MCP surface is controlled by environment variables: +MCP surface is controlled by the environment variables listed under +[Configuration](#environment-variables): ```bash -OS_MCP_SERVER_ENABLED=true # explicit true also auto-starts the stdio transport -OS_MCP_SERVER_NAME=my-crm # override the server name -OS_MCP_SERVER_TRANSPORT=http # override the transport +OS_MCP_SERVER_ENABLED=false # opt the HTTP surface out (it is on by default) +OS_MCP_SERVER_NAME=my-crm # override the server name +OS_MCP_SERVER_TRANSPORT=http # override the transport +OS_MCP_STDIO_ENABLED=true # auto-start the long-lived stdio transport +OS_MCP_STDIO_API_KEY=osk_... # required identity for that transport ``` View MCP messages in client: @@ -619,4 +522,4 @@ Apache-2.0. See [LICENSING.md](../../LICENSING.md). - [Model Context Protocol Specification](https://modelcontextprotocol.io/) - [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) -- [@objectstack/spec/ai](../../spec/src/ai/) +- [@objectstack/spec AI metadata](../spec/src/ai/)