feat(mcp): add stdio + streamable HTTP transports, CLI, and smoke test

- connectStdio(server): wires the MCP server to StdioServerTransport.
- connectHttp(server, port): wires the MCP server to
  StreamableHTTPServerTransport over node:http. Returns a handle with
  port and close().
- bin/pascal-mcp.ts: CLI entrypoint with --stdio (default), --http,
  --port, --scene, --help, --version. Loads node-shims before any
  core import. Logs to stderr (stdio transport uses stdout for
  JSON-RPC).
- scripts/smoke.ts: end-to-end smoke — spawns the stdio bin, connects
  as an MCP Client, exercises get_scene, create_level, validate_scene,
  undo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adrian Perez
2026-04-18 17:51:27 +02:00
co-authored by Claude Opus 4.7
parent 4dbfbb1e1a
commit 441e97b2b6
6 changed files with 367 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
import { afterEach, beforeEach, expect, test } from 'bun:test'
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { SceneBridge } from '../bridge/scene-bridge'
import { createPascalMcpServer } from '../server'
import { connectHttp, type HttpTransportHandle } from './http'
let bridge: SceneBridge
let server: McpServer
let handle: HttpTransportHandle | null = null
beforeEach(() => {
bridge = new SceneBridge()
bridge.loadDefault()
server = createPascalMcpServer({ bridge })
})
afterEach(async () => {
if (handle) {
await handle.close()
handle = null
}
})
test('connectHttp listens on the given port and accepts MCP traffic', async () => {
// Port 0 → OS assigns an ephemeral port.
handle = await connectHttp(server, 0)
expect(handle.port).toBeGreaterThan(0)
const url = new URL(`http://127.0.0.1:${handle.port}/mcp`)
const clientTransport = new StreamableHTTPClientTransport(url)
const client = new Client({ name: 'http-test-client', version: '0.0.0' })
try {
await client.connect(clientTransport)
const tools = await client.listTools()
expect(Array.isArray(tools.tools)).toBe(true)
} finally {
await client.close()
}
})
test('connectHttp close() stops the server', async () => {
handle = await connectHttp(server, 0)
const port = handle.port
await handle.close()
handle = null
// A fresh fetch to the old port should fail (connection refused).
let didConnect = false
try {
await fetch(`http://127.0.0.1:${port}/mcp`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: '{}',
signal: AbortSignal.timeout(500),
})
didConnect = true
} catch {
didConnect = false
}
expect(didConnect).toBe(false)
})