Files
editor/packages/mcp/scripts/smoke.ts
T
Adrian PerezandClaude Opus 4.7 441e97b2b6 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>
2026-04-18 17:51:27 +02:00

82 lines
2.6 KiB
TypeScript

/**
* End-to-end smoke test for @pascal-app/mcp.
*
* Spawns the compiled stdio binary as a child process, connects as an MCP
* client, and exercises a handful of representative tools. This test requires
* the package to be built first (`bun run build`) — the compiled bin is what
* `package.json`'s `bin` entry ships to users.
*
* Run with: bun run scripts/smoke.ts
*/
import { existsSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const BIN_PATH = resolve(__dirname, '../dist/bin/pascal-mcp.js')
async function main(): Promise<void> {
if (!existsSync(BIN_PATH)) {
console.error(`[smoke] bin not found at ${BIN_PATH}`)
console.error('[smoke] run `bun run build` first')
process.exit(1)
}
const transport = new StdioClientTransport({
command: process.execPath,
args: [BIN_PATH, '--stdio'],
stderr: 'inherit',
})
const client = new Client({ name: 'pascal-mcp-smoke', version: '0.0.0' })
try {
await client.connect(transport)
const tools = await client.listTools()
console.log(`[smoke] tools registered: ${tools.tools.length}`)
if (tools.tools.length === 0) {
throw new Error('no tools registered')
}
const getScene = await client.callTool({ name: 'get_scene', arguments: {} })
if (getScene.isError) {
throw new Error(`get_scene failed: ${JSON.stringify(getScene)}`)
}
console.log('[smoke] get_scene: OK')
// create_level — buildingId may not match a real node depending on the
// default scene; we just verify the tool returns a structured response
// rather than crash.
const createLevel = await client.callTool({
name: 'create_level',
arguments: { buildingId: 'tbd', elevation: 1, height: 3 },
})
console.log('[smoke] create_level:', createLevel.isError ? 'structured error (ok)' : 'OK')
const validate = await client.callTool({
name: 'validate_scene',
arguments: {},
})
console.log('[smoke] validate_scene:', validate.isError ? 'ERROR' : 'OK')
const undone = await client.callTool({ name: 'undo', arguments: {} })
console.log('[smoke] undo:', undone.isError ? 'ERROR' : 'OK')
console.log('[smoke] passed')
} finally {
try {
await client.close()
} catch {
// client may already be closed; ignore.
}
}
}
main().catch((err) => {
console.error('[smoke] failed:', err)
process.exit(1)
})