@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 +54 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/server.d.ts +22 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +104 -0
- package/package.json +51 -0
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.
|
package/dist/index.d.ts
ADDED
|
@@ -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";
|
package/dist/server.d.ts
ADDED
|
@@ -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
|
+
}
|