@voyant-travel/mcp 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # @voyant-travel/mcp
2
+
3
+ The in-deployment MCP server for the Voyant framework (voyant#2792). It exposes
4
+ the framework tool registry as a real [Model Context Protocol](https://modelcontextprotocol.io)
5
+ server, mounted as a Hono route group inside the operator deployment at
6
+ `/v1/admin/mcp` — **not** a separate app/worker, and **no Durable Object**.
7
+
8
+ External MCP clients (Claude, ChatGPT, …) connect to that endpoint over the wire.
9
+
10
+ ## API
11
+
12
+ `createMcpHonoApp({ registry, buildContext, serverInfo? })` → a Hono sub-app. Mount it
13
+ at `/v1/admin/mcp` (the `"operator/mcp"` composition entry):
14
+
15
+ - `POST /` — MCP JSON-RPC (`initialize` / `tools/list` / `tools/call`).
16
+ - `GET /manifest` — the contract-versioned tool discovery manifest for remote agents.
17
+
18
+ `buildContext(c)` maps the request's `c.var` (db lease / actor / audience / scope) into
19
+ a `@voyant-travel/tools` `ToolContext`.
20
+
21
+ ## Authentication (external MCP clients)
22
+
23
+ External MCP clients (Claude Desktop, ChatGPT, …) authenticate with a **Bearer
24
+ scoped API key** — the operator's existing `voy_` key pipeline — sent as
25
+ `Authorization: <key>`. No separate OAuth/runner is introduced (voyant#2801): the
26
+ request auth middleware resolves the key into `scopes` + `audience` on `c.var`, and
27
+ this server gates every tool by its `requiredScopes`.
28
+
29
+ Because authorization is per-tool, the `/v1/admin/mcp` surface is exempt from the
30
+ coarse method+path permission guard (`require-actor`, like `_meta`): any
31
+ authenticated key reaches the endpoint, and a key with no relevant scopes simply
32
+ sees an empty `tools/list`. Mint keys with a grant preset (e.g. `agent-customer`)
33
+ to bundle a scope subset with an `audience`.
34
+
35
+ The reserved `apps/agent-runner` / `apps/agent-control-plane` stubs are intentionally
36
+ **not** built out — Voyant ships the tool primitives + this ready-to-use MCP, not an
37
+ agent.
38
+
39
+ ## How it works
40
+
41
+ - **Transport:** `@hono/mcp`'s `StreamableHTTPTransport` (web-standard `Request`/
42
+ `Response`) connected to `@modelcontextprotocol/sdk`'s `McpServer`. The SDK's own
43
+ Node-`http` transport is **not** used.
44
+ - **Stateless, per request:** a fresh `McpServer` + transport per request with
45
+ `{ sessionIdGenerator: undefined, enableJsonResponse: true }`, then
46
+ `server.connect(transport)` → `transport.handleRequest(c)`. No session store, no
47
+ Durable Object — fits the operator's single-worker `nodejs_compat` runtime and
48
+ survives the lazy-route `c.var` re-hydration. Clients must send
49
+ `Accept: application/json, text/event-stream`.
50
+ - **Authorization (D2):** each tool's `requiredScopes` are checked against the caller's
51
+ granted scopes with **AND** semantics (`hasApiKeyPermission`). Unauthorized tools are
52
+ neither listed nor registered on the per-request server, so they cannot be called.
53
+ - **Headless boundary:** the registry returns typed pure data; this adapter wraps it in
54
+ the MCP `CallToolResult` envelope only at the transport edge.
@@ -0,0 +1,2 @@
1
+ export { createMcpHonoApp, type McpHonoAppOptions, type McpServerInfo, } from "./server.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,KAAK,iBAAiB,EACtB,KAAK,aAAa,GACnB,MAAM,aAAa,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { createMcpHonoApp, } from "./server.js";
@@ -0,0 +1,22 @@
1
+ import { type ToolContext, type ToolRegistry } from "@voyant-travel/tools";
2
+ import { type Context, Hono } from "hono";
3
+ export interface McpServerInfo {
4
+ name: string;
5
+ version: string;
6
+ }
7
+ export interface McpHonoAppOptions {
8
+ /** The tool registry to expose. */
9
+ registry: ToolRegistry;
10
+ /** Build the per-request tool context from the Hono context (db/actor/audience/scope). */
11
+ buildContext(c: Context): ToolContext;
12
+ /** MCP server identity advertised in `initialize`. */
13
+ serverInfo?: McpServerInfo;
14
+ }
15
+ /**
16
+ * Build the MCP Hono sub-app. Mount at `/v1/admin/mcp`:
17
+ * - `POST /` — MCP JSON-RPC (`initialize` / `tools/list` / `tools/call`).
18
+ * - `GET /manifest` — the tool discovery manifest (contract-versioned), filtered
19
+ * to what the caller is authorized for.
20
+ */
21
+ export declare function createMcpHonoApp(options: McpHonoAppOptions): Hono;
22
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAkBA,OAAO,EAEL,KAAK,WAAW,EAEhB,KAAK,YAAY,EAClB,MAAM,sBAAsB,CAAA;AAM7B,OAAO,EAAE,KAAK,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAGzC,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,iBAAiB;IAChC,mCAAmC;IACnC,QAAQ,EAAE,YAAY,CAAA;IACtB,0FAA0F;IAC1F,YAAY,CAAC,CAAC,EAAE,OAAO,GAAG,WAAW,CAAA;IACrC,sDAAsD;IACtD,UAAU,CAAC,EAAE,aAAa,CAAA;CAC3B;AAID;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAkCjE"}
package/dist/server.js ADDED
@@ -0,0 +1,104 @@
1
+ /**
2
+ * The in-deployment MCP server (voyant#2792). Exposes a `@voyant-travel/tools`
3
+ * `ToolRegistry` as a real Model Context Protocol server, mounted as a Hono
4
+ * route group inside the operator app at `/v1/admin/mcp` — stateless, no Durable
5
+ * Object (see the Sub-issue 0 spike). External MCP clients connect over the wire.
6
+ *
7
+ * Transport: `@modelcontextprotocol/sdk` `McpServer` connected to `@hono/mcp`'s
8
+ * web-standard `StreamableHTTPTransport`. A fresh server + transport per request
9
+ * keeps it stateless, so the lazy-route `c.var` hydration (db lease / actor /
10
+ * scopes / audience) is all the context we need.
11
+ *
12
+ * Authorization (D2): each tool's `requiredScopes` are checked against the
13
+ * caller's granted scopes with **AND** semantics. Unauthorized tools are neither
14
+ * listed nor registered on the per-request server, so they cannot be called.
15
+ */
16
+ import { StreamableHTTPTransport } from "@hono/mcp";
17
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
18
+ import { TOOL_CONTRACT_VERSION, ToolError, } from "@voyant-travel/tools";
19
+ import { hasApiKeyPermission, permissionStringsToPermissions, } from "@voyant-travel/types/api-keys";
20
+ import { Hono } from "hono";
21
+ import { z } from "zod";
22
+ const DEFAULT_SERVER_INFO = { name: "voyant-mcp", version: "0.1.0" };
23
+ /**
24
+ * Build the MCP Hono sub-app. Mount at `/v1/admin/mcp`:
25
+ * - `POST /` — MCP JSON-RPC (`initialize` / `tools/list` / `tools/call`).
26
+ * - `GET /manifest` — the tool discovery manifest (contract-versioned), filtered
27
+ * to what the caller is authorized for.
28
+ */
29
+ export function createMcpHonoApp(options) {
30
+ const { registry, buildContext } = options;
31
+ const serverInfo = options.serverInfo ?? DEFAULT_SERVER_INFO;
32
+ const app = new Hono();
33
+ app.get("/manifest", (c) => {
34
+ const permissions = callerPermissions(c);
35
+ const tools = registry.list().filter((tool) => isAuthorized(tool.requiredScopes, permissions));
36
+ return c.json({ version: TOOL_CONTRACT_VERSION, serverInfo, tools });
37
+ });
38
+ app.all("/", async (c) => {
39
+ const permissions = callerPermissions(c);
40
+ const ctx = buildContext(c);
41
+ const server = new McpServer(serverInfo);
42
+ for (const entry of registry.list()) {
43
+ if (!isAuthorized(entry.requiredScopes, permissions))
44
+ continue;
45
+ const def = registry.get(entry.name);
46
+ if (!def)
47
+ continue;
48
+ server.tool(entry.name, entry.description, toRawShape(def.inputSchema), (args) => dispatchToResult(registry, entry.name, args, ctx));
49
+ }
50
+ const transport = new StreamableHTTPTransport({
51
+ sessionIdGenerator: undefined,
52
+ enableJsonResponse: true,
53
+ });
54
+ await server.connect(transport);
55
+ return transport.handleRequest(c);
56
+ });
57
+ return app;
58
+ }
59
+ /** Resolve the caller's granted permissions from `c.var.scopes`. */
60
+ function callerPermissions(c) {
61
+ const scopes = c.var.scopes ?? [];
62
+ return permissionStringsToPermissions(scopes);
63
+ }
64
+ /** AND semantics — the caller must hold every one of the tool's required scopes. */
65
+ function isAuthorized(requiredScopes, permissions) {
66
+ return requiredScopes.every((scope) => {
67
+ const [resource, action] = scope.split(":");
68
+ return Boolean(resource && action && hasApiKeyPermission(permissions, resource, action));
69
+ });
70
+ }
71
+ /** Extract a Zod raw shape for the MCP SDK. Tool inputs are objects by convention. */
72
+ function toRawShape(schema) {
73
+ if (schema instanceof z.ZodObject)
74
+ return schema.shape;
75
+ return {};
76
+ }
77
+ /** Dispatch through the registry (validates in + out) and wrap pure data in an MCP envelope. */
78
+ async function dispatchToResult(registry, name, args, ctx) {
79
+ try {
80
+ const data = await registry.dispatch(name, args, ctx);
81
+ return {
82
+ content: [{ type: "text", text: safeStringify(data) }],
83
+ structuredContent: toStructuredContent(data),
84
+ };
85
+ }
86
+ catch (err) {
87
+ const code = err instanceof ToolError ? err.code : "PROVIDER_ERROR";
88
+ const message = err instanceof Error ? err.message : String(err);
89
+ return { isError: true, content: [{ type: "text", text: `[${code}] ${message}` }] };
90
+ }
91
+ }
92
+ function toStructuredContent(data) {
93
+ return data !== null && typeof data === "object" && !Array.isArray(data)
94
+ ? data
95
+ : { result: data };
96
+ }
97
+ function safeStringify(data) {
98
+ try {
99
+ return JSON.stringify(data, null, 2);
100
+ }
101
+ catch {
102
+ return String(data);
103
+ }
104
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@voyant-travel/mcp",
3
+ "version": "0.0.0",
4
+ "license": "Apache-2.0",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./src/index.ts"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "publishConfig": {
13
+ "access": "public",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js",
18
+ "default": "./dist/index.js"
19
+ }
20
+ },
21
+ "main": "./dist/index.js",
22
+ "types": "./dist/index.d.ts"
23
+ },
24
+ "scripts": {
25
+ "typecheck": "tsc -p tsconfig.typecheck.json",
26
+ "lint": "biome check src/",
27
+ "test": "vitest run",
28
+ "build": "tsc -p tsconfig.build.json",
29
+ "clean": "rm -rf dist tsconfig.tsbuildinfo",
30
+ "prepack": "pnpm run build"
31
+ },
32
+ "dependencies": {
33
+ "@hono/mcp": "^0.3.0",
34
+ "@modelcontextprotocol/sdk": "^1.29.0",
35
+ "@voyant-travel/tools": "workspace:^",
36
+ "@voyant-travel/types": "workspace:^",
37
+ "hono": "catalog:",
38
+ "hono-rate-limiter": "^0.5.3",
39
+ "zod": "catalog:"
40
+ },
41
+ "devDependencies": {
42
+ "@voyant-travel/voyant-typescript-config": "workspace:^",
43
+ "typescript": "catalog:",
44
+ "vitest": "catalog:"
45
+ },
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "https://github.com/voyant-travel/voyant.git",
49
+ "directory": "packages/mcp"
50
+ }
51
+ }