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:
co-authored by
Claude Opus 4.7
parent
4dbfbb1e1a
commit
441e97b2b6
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* 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)
|
||||
})
|
||||
Reference in New Issue
Block a user