@happyvertical/smrt-app-mcp 0.40.60 → 0.40.61
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 +50 -2
- package/dist/chunks/protocol-DfGYbPjN.js +94 -0
- package/dist/chunks/protocol-DfGYbPjN.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +32 -62
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/smrt-knowledge.json +4 -3
- package/dist/sveltekit.d.ts +45 -4
- package/dist/sveltekit.js +20 -2
- package/dist/sveltekit.js.map +1 -1
- package/package.json +3 -2
- package/dist/chunks/errors-CHYu0Vr2.js +0 -16
- package/dist/chunks/errors-CHYu0Vr2.js.map +0 -1
package/README.md
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
App-runtime MCP server scaffolding for s-m-r-t apps. Provides:
|
|
4
4
|
|
|
5
|
-
- **Core** — `createMcpAppServer({ smrtOptions, serverInfo, allowedClassNames, publicToolPatterns?, toolPolicy?, workflowAssertions? })` returning `{ listTools, callTool }` wired to `@happyvertical/smrt-core/generators/mcp`.
|
|
6
|
-
- **SvelteKit adapters** (`./sveltekit`) — `
|
|
5
|
+
- **Core** — `createMcpAppServer({ smrtOptions, serverInfo, allowedClassNames, publicToolPatterns?, toolListCache?, toolPolicy?, workflowAssertions? })` returning `{ listTools, callTool }` wired to `@happyvertical/smrt-core/generators/mcp`.
|
|
6
|
+
- **SvelteKit adapters** (`./sveltekit`) — `mountMcpRoute` mounts a modern
|
|
7
|
+
2026-07-28 stateless Streamable HTTP MCP endpoint. The REST-shaped
|
|
8
|
+
`mountMcpToolsRoute` / `mountMcpCallRoute` aliases remain available for one
|
|
9
|
+
release while applications migrate.
|
|
7
10
|
|
|
8
11
|
For piping a deployed app's MCP surface to a local stdio MCP client, see `@happyvertical/smrt-app-cli` — the client-side runtime CLI exposes a `startMcpBridge()` default and a generic `smrt-mcp-bridge` bin.
|
|
9
12
|
|
|
@@ -41,6 +44,51 @@ export const mcpServer = createMcpAppServer({
|
|
|
41
44
|
});
|
|
42
45
|
```
|
|
43
46
|
|
|
47
|
+
```ts
|
|
48
|
+
// src/routes/api/mcp/+server.ts
|
|
49
|
+
import { mountMcpRoute } from '@happyvertical/smrt-app-mcp/sveltekit';
|
|
50
|
+
import { mcpServer } from '$lib/server/mcp';
|
|
51
|
+
export const POST = mountMcpRoute(mcpServer);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`mountMcpRoute` is a modern-only, fetch-style Streamable HTTP endpoint. It
|
|
55
|
+
serves `server/discover`, `tools/list`, and `tools/call` with the SDK's
|
|
56
|
+
2026-07-28 envelope and reports only the `tools` capability. Tool discovery is
|
|
57
|
+
deterministically ordered by name. Stock MCP clients send the required
|
|
58
|
+
`Mcp-Method` header (and `Mcp-Name` for `tools/call`); the mount validates them
|
|
59
|
+
against the JSON-RPC body and returns the protocol `HeaderMismatch` error
|
|
60
|
+
(`-32020`, HTTP 400) for a missing or mismatched header.
|
|
61
|
+
|
|
62
|
+
`tools/list` emits the required cache metadata with a one-day, `private`
|
|
63
|
+
default. Shared (`public`) caching is intentionally exceptional: set
|
|
64
|
+
`toolListCache: { cacheScope: 'public', publicCatalog: true }` only for a
|
|
65
|
+
reviewed catalog where every allowed tool is unauthenticated, read-only, and
|
|
66
|
+
global. The server verifies that shape (including the absence of tenant-scoped
|
|
67
|
+
tools and principal-aware policy) and falls back to `private` otherwise.
|
|
68
|
+
|
|
69
|
+
The route constructs a fresh protocol server for every HTTP request. It does
|
|
70
|
+
not issue or rely on `Mcp-Session-Id`, sticky load-balancer routing, or a held
|
|
71
|
+
SSE connection, so it is safe behind ordinary round-robin deployment. This
|
|
72
|
+
mount exposes no subscription capability; subscription requests are refused as
|
|
73
|
+
a JSON-RPC error before any SSE stream opens. Persist stateful workflow
|
|
74
|
+
progress in application objects, then pass their explicit s-m-r-t object id
|
|
75
|
+
back to the next tool call:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
// `basket_create` returns an object with id "basket-123".
|
|
79
|
+
await client.callTool({
|
|
80
|
+
name: 'basket_additem',
|
|
81
|
+
arguments: { id: 'basket-123', productId: 'product-456' },
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Deprecated REST compatibility
|
|
86
|
+
|
|
87
|
+
For one release, applications that have not moved their route path can retain
|
|
88
|
+
the old handlers below. They are REST-shaped compatibility aliases, not an MCP
|
|
89
|
+
transport, and will be removed after the migration window. Direct calls to a
|
|
90
|
+
tool outside the app allow-list continue to receive the safe 404 behavior.
|
|
91
|
+
|
|
44
92
|
```ts
|
|
45
93
|
// src/routes/api/mcp/tools/+server.ts
|
|
46
94
|
import { mountMcpToolsRoute } from '@happyvertical/smrt-app-mcp/sveltekit';
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { ProtocolError, ProtocolErrorCode, Server } from "@modelcontextprotocol/server";
|
|
2
|
+
//#region src/errors.ts
|
|
3
|
+
var MCP_TOOL_ACCESS_DENIED_CODE = "mcp_tool_access_denied";
|
|
4
|
+
var McpAccessError = class extends Error {
|
|
5
|
+
constructor(status, message, metadata = {}) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.status = status;
|
|
8
|
+
this.metadata = metadata;
|
|
9
|
+
this.name = "McpAccessError";
|
|
10
|
+
}
|
|
11
|
+
status;
|
|
12
|
+
metadata;
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/tools.ts
|
|
16
|
+
function matchesToolPattern(toolName, pattern) {
|
|
17
|
+
if (!pattern) return false;
|
|
18
|
+
if (pattern === "*") return true;
|
|
19
|
+
const parts = pattern.split("*");
|
|
20
|
+
if (parts.length === 1) return toolName === pattern;
|
|
21
|
+
let cursor = 0;
|
|
22
|
+
if (parts[0] && !toolName.startsWith(parts[0])) return false;
|
|
23
|
+
for (const part of parts) {
|
|
24
|
+
if (!part) continue;
|
|
25
|
+
const index = toolName.indexOf(part, cursor);
|
|
26
|
+
if (index < 0) return false;
|
|
27
|
+
cursor = index + part.length;
|
|
28
|
+
}
|
|
29
|
+
const last = parts.at(-1);
|
|
30
|
+
return !last || toolName.endsWith(last);
|
|
31
|
+
}
|
|
32
|
+
function isReadOnlyToolName(toolName) {
|
|
33
|
+
return toolName.endsWith("_list") || toolName.endsWith("_get");
|
|
34
|
+
}
|
|
35
|
+
function isPublicToolName(toolName, patterns) {
|
|
36
|
+
return isReadOnlyToolName(toolName) && patterns.some((pattern) => matchesToolPattern(toolName, pattern));
|
|
37
|
+
}
|
|
38
|
+
function classNamePrefixes(classNames) {
|
|
39
|
+
return new Set(classNames.map((className) => `${className.toLowerCase()}_`));
|
|
40
|
+
}
|
|
41
|
+
function isAllowedCoreTool(toolName, prefixes) {
|
|
42
|
+
for (const prefix of prefixes) if (toolName.startsWith(prefix)) return true;
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
function compareMcpToolNames(left, right) {
|
|
46
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/protocol.ts
|
|
50
|
+
var DEFAULT_TOOL_LIST_CACHE_HINT = {
|
|
51
|
+
ttlMs: 864e5,
|
|
52
|
+
cacheScope: "private"
|
|
53
|
+
};
|
|
54
|
+
async function resolvePrincipal(option, context) {
|
|
55
|
+
if (typeof option === "function") return await option(context) ?? null;
|
|
56
|
+
return option ?? null;
|
|
57
|
+
}
|
|
58
|
+
function createMcpProtocolServer(appServer, options = {}) {
|
|
59
|
+
const server = new Server(appServer.serverInfo, {
|
|
60
|
+
capabilities: { tools: {} },
|
|
61
|
+
cacheHints: { "tools/list": DEFAULT_TOOL_LIST_CACHE_HINT }
|
|
62
|
+
});
|
|
63
|
+
server.setRequestHandler("tools/list", async (_request, context) => {
|
|
64
|
+
const principal = await resolvePrincipal(options.principal, context);
|
|
65
|
+
const cacheHint = await appServer.getToolsListCacheHint?.() ?? DEFAULT_TOOL_LIST_CACHE_HINT;
|
|
66
|
+
return {
|
|
67
|
+
tools: [...await appServer.listTools({ principal })].sort((left, right) => compareMcpToolNames(left.name, right.name)),
|
|
68
|
+
...cacheHint
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
server.setRequestHandler("tools/call", async (request, context) => {
|
|
72
|
+
try {
|
|
73
|
+
return await appServer.callTool({
|
|
74
|
+
name: request.params.name,
|
|
75
|
+
arguments: request.params.arguments,
|
|
76
|
+
principal: await resolvePrincipal(options.principal, context)
|
|
77
|
+
});
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (error instanceof McpAccessError) {
|
|
80
|
+
const { code, retryable } = error.metadata;
|
|
81
|
+
throw new ProtocolError(error.status === 404 ? ProtocolErrorCode.InvalidParams : ProtocolErrorCode.InvalidRequest, error.message, {
|
|
82
|
+
...typeof code === "string" ? { code } : {},
|
|
83
|
+
...typeof retryable === "boolean" ? { retryable } : {}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
return server;
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
export { isPublicToolName as a, MCP_TOOL_ACCESS_DENIED_CODE as c, isAllowedCoreTool as i, McpAccessError as l, classNamePrefixes as n, isReadOnlyToolName as o, compareMcpToolNames as r, matchesToolPattern as s, createMcpProtocolServer as t };
|
|
93
|
+
|
|
94
|
+
//# sourceMappingURL=protocol-DfGYbPjN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol-DfGYbPjN.js","names":[],"sources":["../../src/errors.ts","../../src/tools.ts","../../src/protocol.ts"],"sourcesContent":["/** Machine-readable code for a principal policy denial. */\nexport const MCP_TOOL_ACCESS_DENIED_CODE = 'mcp_tool_access_denied';\n\n/**\n * Metadata that is safe to expose for an app-MCP access failure. Policy\n * implementations must not place principal, scope, tool, or internal-error\n * details here.\n */\nexport interface McpAccessErrorMetadata {\n code?: string;\n retryable?: boolean;\n}\n\n/**\n * Error returned by the MCP app server when a caller tries to use a tool\n * they are not allowed to access. The HTTP layer should map `status` onto\n * the response status code.\n */\nexport class McpAccessError extends Error {\n constructor(\n readonly status: number,\n message: string,\n readonly metadata: McpAccessErrorMetadata = {},\n ) {\n super(message);\n this.name = 'McpAccessError';\n }\n}\n","/**\n * Tool-name policy helpers — used by `McpAppServer` to filter the full set\n * of generated tools down to what the calling principal is allowed to see,\n * and to decide whether an unauthenticated tool call should be permitted.\n *\n * @packageDocumentation\n */\n\n/**\n * Match a tool name against a glob-ish pattern with `*` wildcards.\n *\n * - Empty pattern → never matches.\n * - `*` → matches everything.\n * - `prefix_*` → matches anything starting with `prefix_`.\n * - `*_suffix` → matches anything ending with `_suffix`.\n * - `a_*_b` → matches any name containing `a_`, then any text, then `_b`.\n *\n * No regex characters are special besides `*` — the input is treated as a\n * literal string with star wildcards.\n */\nexport function matchesToolPattern(toolName: string, pattern: string): boolean {\n if (!pattern) return false;\n if (pattern === '*') return true;\n\n const parts = pattern.split('*');\n if (parts.length === 1) return toolName === pattern;\n\n let cursor = 0;\n if (parts[0] && !toolName.startsWith(parts[0])) return false;\n for (const part of parts) {\n if (!part) continue;\n const index = toolName.indexOf(part, cursor);\n if (index < 0) return false;\n cursor = index + part.length;\n }\n\n const last = parts.at(-1);\n return !last || toolName.endsWith(last);\n}\n\n/**\n * Read-only tool detection. Generated SMRT MCP tools follow the naming\n * convention `<class>_<verb>`; we treat `_list` and `_get` as read-only.\n */\nexport function isReadOnlyToolName(toolName: string): boolean {\n return toolName.endsWith('_list') || toolName.endsWith('_get');\n}\n\n/**\n * Check whether a tool name is currently allowed for unauthenticated callers\n * given the configured public-tool patterns. Only read-only tools may ever\n * be public, regardless of pattern.\n */\nexport function isPublicToolName(\n toolName: string,\n patterns: readonly string[],\n): boolean {\n return (\n isReadOnlyToolName(toolName) &&\n patterns.some((pattern) => matchesToolPattern(toolName, pattern))\n );\n}\n\n/**\n * Lower-case `<class>_` prefixes the app considers \"allowed core tools\"\n * given a list of SMRT class names. Used to build the allow-list for\n * `McpAppServer.listTools`.\n */\nexport function classNamePrefixes(\n classNames: readonly string[],\n): ReadonlySet<string> {\n return new Set(classNames.map((className) => `${className.toLowerCase()}_`));\n}\n\n/**\n * Whether a given tool name starts with any of the configured class\n * prefixes.\n */\nexport function isAllowedCoreTool(\n toolName: string,\n prefixes: ReadonlySet<string>,\n): boolean {\n for (const prefix of prefixes) {\n if (toolName.startsWith(prefix)) return true;\n }\n return false;\n}\n\n/**\n * Compare tool names by Unicode code unit, rather than the host locale, so a\n * catalog has one byte-stable order across every runtime.\n */\nexport function compareMcpToolNames(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0;\n}\n","/** MCP SDK v2 protocol adapter for the framework-neutral app server core. */\nimport {\n type CallToolResult,\n ProtocolError,\n ProtocolErrorCode,\n Server,\n type ServerContext,\n type Tool,\n} from '@modelcontextprotocol/server';\nimport { McpAccessError } from './errors.js';\nimport type { McpAppPrincipal, McpAppServer } from './server.js';\nimport { compareMcpToolNames } from './tools.js';\n\nconst DEFAULT_TOOL_LIST_CACHE_HINT = {\n ttlMs: 86_400_000,\n cacheScope: 'private' as const,\n};\n\nexport interface McpProtocolServerOptions {\n /** Resolve the authenticated application principal for each MCP request. */\n principal?:\n | McpAppPrincipal\n | null\n | ((\n context: ServerContext,\n ) => McpAppPrincipal | null | Promise<McpAppPrincipal | null>);\n}\n\nasync function resolvePrincipal(\n option: McpProtocolServerOptions['principal'],\n context: ServerContext,\n): Promise<McpAppPrincipal | null> {\n if (typeof option === 'function') return (await option(context)) ?? null;\n return option ?? null;\n}\n\n/**\n * Adapt an app MCP core to the SDK v2 low-level server protocol.\n *\n * Transport ownership remains with the caller. In particular, this does not\n * add a production HTTP endpoint; it is safe to compose with `serveStdio` or\n * `createMcpHandler` in a deployment that supplies its own authentication.\n */\nexport function createMcpProtocolServer(\n appServer: McpAppServer,\n options: McpProtocolServerOptions = {},\n): Server {\n const server = new Server(appServer.serverInfo, {\n capabilities: { tools: {} },\n cacheHints: { 'tools/list': DEFAULT_TOOL_LIST_CACHE_HINT },\n });\n\n server.setRequestHandler('tools/list', async (_request, context) => {\n const principal = await resolvePrincipal(options.principal, context);\n const cacheHint =\n (await appServer.getToolsListCacheHint?.()) ??\n DEFAULT_TOOL_LIST_CACHE_HINT;\n return {\n tools: [...(await appServer.listTools({ principal }))].sort(\n (left, right) => compareMcpToolNames(left.name, right.name),\n ) as Tool[],\n ...cacheHint,\n };\n });\n\n server.setRequestHandler('tools/call', async (request, context) => {\n try {\n return (await appServer.callTool({\n name: request.params.name,\n arguments: request.params.arguments,\n principal: await resolvePrincipal(options.principal, context),\n })) as CallToolResult;\n } catch (error) {\n if (error instanceof McpAccessError) {\n const { code, retryable } = error.metadata;\n throw new ProtocolError(\n error.status === 404\n ? ProtocolErrorCode.InvalidParams\n : ProtocolErrorCode.InvalidRequest,\n error.message,\n {\n ...(typeof code === 'string' ? { code } : {}),\n ...(typeof retryable === 'boolean' ? { retryable } : {}),\n },\n );\n }\n throw error;\n }\n });\n\n return server;\n}\n"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -75,6 +75,12 @@ export declare interface CreateMcpAppServerOptions {
|
|
|
75
75
|
* (everything requires auth).
|
|
76
76
|
*/
|
|
77
77
|
publicToolPatterns?: McpPublicToolPatternsThunk;
|
|
78
|
+
/**
|
|
79
|
+
* Cache policy for the MCP tools/list result. Public caching is honored only
|
|
80
|
+
* when this explicitly opts in and every allowed tool is a non-tenant,
|
|
81
|
+
* unauthenticated read-only tool with no principal-aware policy.
|
|
82
|
+
*/
|
|
83
|
+
toolListCache?: McpToolListCacheOptions;
|
|
78
84
|
/**
|
|
79
85
|
* Optional generic principal-aware tool policy. It is evaluated for every
|
|
80
86
|
* tool that passes the app allow-list and base public/authenticated policy,
|
|
@@ -186,6 +192,8 @@ export declare interface McpAppPrincipal {
|
|
|
186
192
|
export declare interface McpAppServer {
|
|
187
193
|
listTools(input: ListToolsInput): Promise<MCPTool[]>;
|
|
188
194
|
callTool(input: CallToolInput): Promise<MCPResponse>;
|
|
195
|
+
/** Cache policy for protocol tools/list responses. */
|
|
196
|
+
getToolsListCacheHint?(): Promise<McpToolListCacheHint>;
|
|
189
197
|
/** Read-only view of the configured server identity. */
|
|
190
198
|
readonly serverInfo: CreateMcpAppServerOptions['serverInfo'];
|
|
191
199
|
}
|
|
@@ -210,6 +218,23 @@ export declare type McpPublicToolPatternsThunk = () => readonly string[];
|
|
|
210
218
|
*/
|
|
211
219
|
export declare type McpSmrtOptionsThunk = () => Record<string, unknown>;
|
|
212
220
|
|
|
221
|
+
export declare interface McpToolListCacheHint {
|
|
222
|
+
ttlMs: number;
|
|
223
|
+
cacheScope: 'private' | 'public';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export declare interface McpToolListCacheOptions {
|
|
227
|
+
/** Cache lifetime in milliseconds. Defaults to one day for a deploy-static catalog. */
|
|
228
|
+
ttlMs?: number;
|
|
229
|
+
/** Requested cache visibility. Defaults to private. */
|
|
230
|
+
cacheScope?: 'private' | 'public';
|
|
231
|
+
/**
|
|
232
|
+
* Explicit attestation that every allowed tool is global, unauthenticated,
|
|
233
|
+
* and safe to share through an intermediary cache.
|
|
234
|
+
*/
|
|
235
|
+
publicCatalog?: true;
|
|
236
|
+
}
|
|
237
|
+
|
|
213
238
|
/**
|
|
214
239
|
* Per-tool access policy. Return `true` to expose/allow the tool and `false`
|
|
215
240
|
* to hide it from discovery and deny a direct call. A thrown error is treated
|
package/dist/index.js
CHANGED
|
@@ -1,72 +1,32 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { MCPGenerator } from "@happyvertical/smrt-core/generators/mcp";
|
|
4
|
-
//#region src/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
return await appServer.callTool({
|
|
15
|
-
name: request.params.name,
|
|
16
|
-
arguments: request.params.arguments,
|
|
17
|
-
principal: await resolvePrincipal(options.principal, context)
|
|
18
|
-
});
|
|
19
|
-
} catch (error) {
|
|
20
|
-
if (error instanceof McpAccessError) {
|
|
21
|
-
const { code, retryable } = error.metadata;
|
|
22
|
-
throw new ProtocolError(error.status === 404 ? ProtocolErrorCode.InvalidParams : ProtocolErrorCode.InvalidRequest, error.message, {
|
|
23
|
-
...typeof code === "string" ? { code } : {},
|
|
24
|
-
...typeof retryable === "boolean" ? { retryable } : {}
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
throw error;
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
return server;
|
|
1
|
+
import { a as isPublicToolName, c as MCP_TOOL_ACCESS_DENIED_CODE, i as isAllowedCoreTool, l as McpAccessError, n as classNamePrefixes, o as isReadOnlyToolName, r as compareMcpToolNames, s as matchesToolPattern, t as createMcpProtocolServer } from "./chunks/protocol-DfGYbPjN.js";
|
|
2
|
+
import { ObjectRegistry, isTenantScopedClassResolved } from "@happyvertical/smrt-core";
|
|
3
|
+
import { MCPGenerator, MCP_STABLE_CATALOG_TTL_MS } from "@happyvertical/smrt-core/generators/mcp";
|
|
4
|
+
//#region src/server.ts
|
|
5
|
+
function configuredToolListCacheHint(options) {
|
|
6
|
+
const ttlMs = options?.ttlMs ?? MCP_STABLE_CATALOG_TTL_MS;
|
|
7
|
+
if (!Number.isSafeInteger(ttlMs) || ttlMs < 0) throw new RangeError("MCP tools/list cache ttlMs must be a non-negative safe integer.");
|
|
8
|
+
if (options?.cacheScope !== void 0 && options.cacheScope !== "private" && options.cacheScope !== "public") throw new RangeError("MCP tools/list cacheScope must be 'private' or 'public'.");
|
|
9
|
+
return {
|
|
10
|
+
ttlMs,
|
|
11
|
+
cacheScope: options?.cacheScope === "public" && options.publicCatalog === true ? "public" : "private"
|
|
12
|
+
};
|
|
31
13
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
let cursor = 0;
|
|
40
|
-
if (parts[0] && !toolName.startsWith(parts[0])) return false;
|
|
41
|
-
for (const part of parts) {
|
|
42
|
-
if (!part) continue;
|
|
43
|
-
const index = toolName.indexOf(part, cursor);
|
|
44
|
-
if (index < 0) return false;
|
|
45
|
-
cursor = index + part.length;
|
|
14
|
+
function isTenantScopedTool(tool) {
|
|
15
|
+
const separator = tool.name.indexOf("_");
|
|
16
|
+
if (separator <= 0) return false;
|
|
17
|
+
const objectName = tool.name.slice(0, separator).toLowerCase();
|
|
18
|
+
for (const [key, classInfo] of ObjectRegistry.getAllClasses()) {
|
|
19
|
+
const name = classInfo.name || key;
|
|
20
|
+
if (name.toLowerCase() === objectName) return ObjectRegistry.isTenantScoped(name) || isTenantScopedClassResolved(name);
|
|
46
21
|
}
|
|
47
|
-
const last = parts.at(-1);
|
|
48
|
-
return !last || toolName.endsWith(last);
|
|
49
|
-
}
|
|
50
|
-
function isReadOnlyToolName(toolName) {
|
|
51
|
-
return toolName.endsWith("_list") || toolName.endsWith("_get");
|
|
52
|
-
}
|
|
53
|
-
function isPublicToolName(toolName, patterns) {
|
|
54
|
-
return isReadOnlyToolName(toolName) && patterns.some((pattern) => matchesToolPattern(toolName, pattern));
|
|
55
|
-
}
|
|
56
|
-
function classNamePrefixes(classNames) {
|
|
57
|
-
return new Set(classNames.map((className) => `${className.toLowerCase()}_`));
|
|
58
|
-
}
|
|
59
|
-
function isAllowedCoreTool(toolName, prefixes) {
|
|
60
|
-
for (const prefix of prefixes) if (toolName.startsWith(prefix)) return true;
|
|
61
22
|
return false;
|
|
62
23
|
}
|
|
63
|
-
//#endregion
|
|
64
|
-
//#region src/server.ts
|
|
65
24
|
function createMcpAppServer(options) {
|
|
66
25
|
const allowedPrefixes = classNamePrefixes(options.allowedClassNames);
|
|
67
26
|
const getPublicPatterns = options.publicToolPatterns ?? (() => []);
|
|
68
27
|
const toolPolicy = options.toolPolicy;
|
|
69
28
|
const workflowAssertions = options.workflowAssertions ?? {};
|
|
29
|
+
const requestedToolListCacheHint = configuredToolListCacheHint(options.toolListCache);
|
|
70
30
|
function userForGenerator(principal) {
|
|
71
31
|
if (!principal?.id) return void 0;
|
|
72
32
|
return {
|
|
@@ -90,7 +50,7 @@ function createMcpAppServer(options) {
|
|
|
90
50
|
return input.user ?? null;
|
|
91
51
|
}
|
|
92
52
|
async function allowedTools() {
|
|
93
|
-
return (await makeGenerator().generateTools()).filter((tool) => isAllowedCoreTool(tool.name, allowedPrefixes));
|
|
53
|
+
return (await makeGenerator().generateTools()).filter((tool) => isAllowedCoreTool(tool.name.toLowerCase(), allowedPrefixes)).sort((left, right) => compareMcpToolNames(left.name, right.name));
|
|
94
54
|
}
|
|
95
55
|
function passesBasePolicy(tool, principal, publicPatterns) {
|
|
96
56
|
if (principal) return true;
|
|
@@ -117,11 +77,20 @@ function createMcpAppServer(options) {
|
|
|
117
77
|
}));
|
|
118
78
|
return tools.filter((_, index) => visible[index]);
|
|
119
79
|
}
|
|
80
|
+
async function getToolsListCacheHint() {
|
|
81
|
+
if (requestedToolListCacheHint.cacheScope !== "public") return requestedToolListCacheHint;
|
|
82
|
+
const tools = await allowedTools();
|
|
83
|
+
const publicPatterns = getPublicPatterns();
|
|
84
|
+
return !toolPolicy && tools.every((tool) => isPublicToolName(tool.name, publicPatterns) && !isTenantScopedTool(tool)) ? requestedToolListCacheHint : {
|
|
85
|
+
...requestedToolListCacheHint,
|
|
86
|
+
cacheScope: "private"
|
|
87
|
+
};
|
|
88
|
+
}
|
|
120
89
|
async function callTool(input) {
|
|
121
90
|
const args = input.arguments ?? {};
|
|
122
91
|
const principal = principalForCall(input);
|
|
123
92
|
const tool = (await allowedTools()).find((candidate) => candidate.name === input.name);
|
|
124
|
-
if (!tool) throw new McpAccessError(404,
|
|
93
|
+
if (!tool) throw new McpAccessError(404, "Unknown MCP tool.");
|
|
125
94
|
if (!passesBasePolicy(tool, principal, principal ? void 0 : getPublicPatterns())) throw new McpAccessError(401, `Authentication is required for MCP tool: ${input.name}`);
|
|
126
95
|
if (!await passesToolPolicy(tool, principal)) throw new McpAccessError(403, "MCP tool access is not permitted.", {
|
|
127
96
|
code: MCP_TOOL_ACCESS_DENIED_CODE,
|
|
@@ -140,6 +109,7 @@ function createMcpAppServer(options) {
|
|
|
140
109
|
return {
|
|
141
110
|
listTools,
|
|
142
111
|
callTool,
|
|
112
|
+
getToolsListCacheHint,
|
|
143
113
|
serverInfo: options.serverInfo
|
|
144
114
|
};
|
|
145
115
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/protocol.ts","../src/tools.ts","../src/server.ts"],"sourcesContent":["/** MCP SDK v2 protocol adapter for the framework-neutral app server core. */\nimport {\n type CallToolResult,\n ProtocolError,\n ProtocolErrorCode,\n Server,\n type ServerContext,\n type Tool,\n} from '@modelcontextprotocol/server';\nimport { McpAccessError } from './errors.js';\nimport type { McpAppPrincipal, McpAppServer } from './server.js';\n\nexport interface McpProtocolServerOptions {\n /** Resolve the authenticated application principal for each MCP request. */\n principal?:\n | McpAppPrincipal\n | null\n | ((\n context: ServerContext,\n ) => McpAppPrincipal | null | Promise<McpAppPrincipal | null>);\n}\n\nasync function resolvePrincipal(\n option: McpProtocolServerOptions['principal'],\n context: ServerContext,\n): Promise<McpAppPrincipal | null> {\n if (typeof option === 'function') return (await option(context)) ?? null;\n return option ?? null;\n}\n\n/**\n * Adapt an app MCP core to the SDK v2 low-level server protocol.\n *\n * Transport ownership remains with the caller. In particular, this does not\n * add a production HTTP endpoint; it is safe to compose with `serveStdio` or\n * `createMcpHandler` in a deployment that supplies its own authentication.\n */\nexport function createMcpProtocolServer(\n appServer: McpAppServer,\n options: McpProtocolServerOptions = {},\n): Server {\n const server = new Server(appServer.serverInfo, {\n capabilities: { tools: {} },\n });\n\n server.setRequestHandler('tools/list', async (_request, context) => ({\n tools: (await appServer.listTools({\n principal: await resolvePrincipal(options.principal, context),\n })) as Tool[],\n }));\n\n server.setRequestHandler('tools/call', async (request, context) => {\n try {\n return (await appServer.callTool({\n name: request.params.name,\n arguments: request.params.arguments,\n principal: await resolvePrincipal(options.principal, context),\n })) as CallToolResult;\n } catch (error) {\n if (error instanceof McpAccessError) {\n const { code, retryable } = error.metadata;\n throw new ProtocolError(\n error.status === 404\n ? ProtocolErrorCode.InvalidParams\n : ProtocolErrorCode.InvalidRequest,\n error.message,\n {\n ...(typeof code === 'string' ? { code } : {}),\n ...(typeof retryable === 'boolean' ? { retryable } : {}),\n },\n );\n }\n throw error;\n }\n });\n\n return server;\n}\n","/**\n * Tool-name policy helpers — used by `McpAppServer` to filter the full set\n * of generated tools down to what the calling principal is allowed to see,\n * and to decide whether an unauthenticated tool call should be permitted.\n *\n * @packageDocumentation\n */\n\n/**\n * Match a tool name against a glob-ish pattern with `*` wildcards.\n *\n * - Empty pattern → never matches.\n * - `*` → matches everything.\n * - `prefix_*` → matches anything starting with `prefix_`.\n * - `*_suffix` → matches anything ending with `_suffix`.\n * - `a_*_b` → matches any name containing `a_`, then any text, then `_b`.\n *\n * No regex characters are special besides `*` — the input is treated as a\n * literal string with star wildcards.\n */\nexport function matchesToolPattern(toolName: string, pattern: string): boolean {\n if (!pattern) return false;\n if (pattern === '*') return true;\n\n const parts = pattern.split('*');\n if (parts.length === 1) return toolName === pattern;\n\n let cursor = 0;\n if (parts[0] && !toolName.startsWith(parts[0])) return false;\n for (const part of parts) {\n if (!part) continue;\n const index = toolName.indexOf(part, cursor);\n if (index < 0) return false;\n cursor = index + part.length;\n }\n\n const last = parts.at(-1);\n return !last || toolName.endsWith(last);\n}\n\n/**\n * Read-only tool detection. Generated SMRT MCP tools follow the naming\n * convention `<class>_<verb>`; we treat `_list` and `_get` as read-only.\n */\nexport function isReadOnlyToolName(toolName: string): boolean {\n return toolName.endsWith('_list') || toolName.endsWith('_get');\n}\n\n/**\n * Check whether a tool name is currently allowed for unauthenticated callers\n * given the configured public-tool patterns. Only read-only tools may ever\n * be public, regardless of pattern.\n */\nexport function isPublicToolName(\n toolName: string,\n patterns: readonly string[],\n): boolean {\n return (\n isReadOnlyToolName(toolName) &&\n patterns.some((pattern) => matchesToolPattern(toolName, pattern))\n );\n}\n\n/**\n * Lower-case `<class>_` prefixes the app considers \"allowed core tools\"\n * given a list of SMRT class names. Used to build the allow-list for\n * `McpAppServer.listTools`.\n */\nexport function classNamePrefixes(\n classNames: readonly string[],\n): ReadonlySet<string> {\n return new Set(classNames.map((className) => `${className.toLowerCase()}_`));\n}\n\n/**\n * Whether a given tool name starts with any of the configured class\n * prefixes.\n */\nexport function isAllowedCoreTool(\n toolName: string,\n prefixes: ReadonlySet<string>,\n): boolean {\n for (const prefix of prefixes) {\n if (toolName.startsWith(prefix)) return true;\n }\n return false;\n}\n","/**\n * `createMcpAppServer` returns the framework-agnostic core that backs an\n * app's HTTP MCP endpoints and stdio bridge. It wraps `MCPGenerator` from\n * `@happyvertical/smrt-core` with:\n *\n * - an allow-list of SMRT class names (so apps publish a subset of their\n * objects, not everything decorated with `@smrt()`),\n * - a public-tool policy for unauthenticated callers (read-only patterns\n * via `publicToolPatterns`),\n * - an optional principal-aware `toolPolicy`, used for both discovery and\n * direct calls,\n * - a pluggable `workflowAssertions` hook so apps can guard their own\n * domain-specific tool calls (e.g. \"approval requires an authenticated\n * user\") without that policy living in this package.\n *\n * @packageDocumentation\n */\n\nimport type {\n MCPConfig,\n MCPResponse,\n MCPTool,\n} from '@happyvertical/smrt-core/generators/mcp';\nimport { MCPGenerator } from '@happyvertical/smrt-core/generators/mcp';\nimport { MCP_TOOL_ACCESS_DENIED_CODE, McpAccessError } from './errors.js';\nimport {\n classNamePrefixes,\n isAllowedCoreTool,\n isPublicToolName,\n} from './tools.js';\n\n/**\n * Generic authenticated caller information available to app-MCP policy.\n *\n * `kind`, `roles`, and `scopes` are deliberately unconstrained so an app can\n * represent a human or a scoped service without this package encoding an\n * application's identity or capability model. A missing principal means the\n * request is unauthenticated.\n */\nexport interface McpAppPrincipal {\n id?: string;\n kind?: string;\n roles?: string[];\n scopes?: string[];\n}\n\n/** Minimal legacy user shape used for generated tool-call attribution. */\nexport interface McpAppUser extends McpAppPrincipal {\n id: string;\n}\n\n/** Context supplied to the optional per-tool principal policy. */\nexport interface McpToolPolicyContext {\n principal: McpAppPrincipal | null;\n tool: MCPTool;\n}\n\n/**\n * Per-tool access policy. Return `true` to expose/allow the tool and `false`\n * to hide it from discovery and deny a direct call. A thrown error is treated\n * as a denial so policy implementation details cannot escape the app-MCP\n * boundary.\n */\nexport type McpToolPolicy = (\n context: McpToolPolicyContext,\n) => boolean | Promise<boolean>;\n\n/**\n * Workflow assertion hook signature. Throw `McpAccessError` to reject the\n * call. Implementations may mutate `args` in place to inject server-trusted\n * fields (e.g. clamping `approvedByUserId` to the authenticated user's id).\n */\nexport type McpWorkflowAssertion = (\n args: Record<string, unknown>,\n user: McpAppUser | null,\n) => void;\n\n/**\n * SMRT options thunk — returns the `{ db }` (and similar) bag to pass into\n * MCPGenerator's per-request context. A function is used so apps can lazily\n * resolve env vars at call time.\n */\nexport type McpSmrtOptionsThunk = () => Record<string, unknown>;\n\n/** Public-tool patterns thunk — same lazy-evaluation rationale. */\nexport type McpPublicToolPatternsThunk = () => readonly string[];\n\n/**\n * Options for `createMcpAppServer`.\n */\nexport interface CreateMcpAppServerOptions {\n /** SMRT context bag (db, etc.) passed to MCPGenerator per call. */\n smrtOptions: McpSmrtOptionsThunk;\n /** Server identity surfaced in the MCP protocol. */\n serverInfo: Required<Pick<MCPConfig, 'name' | 'version'>> &\n Pick<MCPConfig, 'description'>;\n /**\n * SMRT class names the app wants to publish. Tools whose name does not\n * start with any of these classes (lowercased + underscore) are filtered\n * out, even if SMRT generated them.\n */\n allowedClassNames: readonly string[];\n /**\n * Optional thunk returning glob-ish patterns for read-only tools that\n * unauthenticated callers are allowed to use. Defaults to an empty list\n * (everything requires auth).\n */\n publicToolPatterns?: McpPublicToolPatternsThunk;\n /**\n * Optional generic principal-aware tool policy. It is evaluated for every\n * tool that passes the app allow-list and base public/authenticated policy,\n * for both discovery and a direct call.\n */\n toolPolicy?: McpToolPolicy;\n /**\n * Optional per-tool guards. Keyed by tool name. The assertion runs after\n * tool resolution and before `MCPGenerator.handleToolCall`; throwing\n * `McpAccessError` aborts the call with the error's status. Implementations\n * may mutate `args` to inject trusted fields.\n */\n workflowAssertions?: Record<string, McpWorkflowAssertion>;\n}\n\n/** Tool listing inputs. */\nexport interface ListToolsInput {\n /** Caller used for public/authenticated and optional tool-policy checks. */\n principal?: McpAppPrincipal | null;\n /**\n * Backwards-compatible authenticated marker. New mounts should pass\n * `principal` so discovery and direct calls use the same identity.\n */\n authenticated?: boolean;\n}\n\n/** Tool call inputs. */\nexport interface CallToolInput {\n name: string;\n arguments?: Record<string, unknown>;\n /** Caller used for public/authenticated and optional tool-policy checks. */\n principal?: McpAppPrincipal | null;\n /**\n * Backwards-compatible user input. New callers should pass `principal`.\n * Null/undefined means unauthenticated.\n */\n user?: McpAppUser | null;\n}\n\n/** Shape returned by `createMcpAppServer`. */\nexport interface McpAppServer {\n listTools(input: ListToolsInput): Promise<MCPTool[]>;\n callTool(input: CallToolInput): Promise<MCPResponse>;\n /** Read-only view of the configured server identity. */\n readonly serverInfo: CreateMcpAppServerOptions['serverInfo'];\n}\n\n/**\n * Build the app-runtime MCP server core. The returned object is intentionally\n * framework-agnostic: HTTP wrappers live in `@happyvertical/smrt-app-mcp/sveltekit`\n * and a stdio bridge lives in `@happyvertical/smrt-app-mcp/bin/smrt-mcp-bridge`.\n */\nexport function createMcpAppServer(\n options: CreateMcpAppServerOptions,\n): McpAppServer {\n const allowedPrefixes = classNamePrefixes(options.allowedClassNames);\n const getPublicPatterns =\n options.publicToolPatterns ?? ((): readonly string[] => []);\n const toolPolicy = options.toolPolicy;\n const workflowAssertions = options.workflowAssertions ?? {};\n\n function userForGenerator(\n principal?: McpAppPrincipal | null,\n ): McpAppUser | undefined {\n if (!principal?.id) return undefined;\n return { id: principal.id, roles: principal.roles };\n }\n\n function makeGenerator(principal?: McpAppPrincipal | null): MCPGenerator {\n const user = userForGenerator(principal);\n return new MCPGenerator(options.serverInfo as MCPConfig, {\n ...options.smrtOptions(),\n user,\n });\n }\n\n function principalForList(input: ListToolsInput): McpAppPrincipal | null {\n if (input.principal !== undefined) return input.principal;\n // Preserve callers of the original boolean API without inventing an id.\n return input.authenticated ? {} : null;\n }\n\n function principalForCall(input: CallToolInput): McpAppPrincipal | null {\n if (input.principal !== undefined) return input.principal;\n return input.user ?? null;\n }\n\n async function allowedTools(): Promise<MCPTool[]> {\n const tools = await makeGenerator().generateTools();\n return tools.filter((tool) =>\n isAllowedCoreTool(tool.name, allowedPrefixes),\n );\n }\n\n function passesBasePolicy(\n tool: MCPTool,\n principal: McpAppPrincipal | null,\n publicPatterns?: readonly string[],\n ): boolean {\n if (principal) return true;\n return isPublicToolName(tool.name, publicPatterns ?? []);\n }\n\n async function passesToolPolicy(\n tool: MCPTool,\n principal: McpAppPrincipal | null,\n ): Promise<boolean> {\n if (!toolPolicy) return true;\n try {\n return Boolean(await toolPolicy({ principal, tool }));\n } catch {\n // A policy failure must fail closed and never leak implementation detail.\n return false;\n }\n }\n\n async function listTools(input: ListToolsInput): Promise<MCPTool[]> {\n const principal = principalForList(input);\n const tools = await allowedTools();\n // Keep the lazy thunk per request, not per tool. Besides avoiding repeated\n // work, this gives one consistent public surface when a thunk reads a\n // dynamic source such as an environment-backed configuration.\n const publicPatterns = principal ? undefined : getPublicPatterns();\n const visible = await Promise.all(\n tools.map(async (tool) => {\n if (!passesBasePolicy(tool, principal, publicPatterns)) return false;\n return passesToolPolicy(tool, principal);\n }),\n );\n return tools.filter((_, index) => visible[index]);\n }\n\n async function callTool(input: CallToolInput): Promise<MCPResponse> {\n const args = input.arguments ?? {};\n const principal = principalForCall(input);\n const tools = await allowedTools();\n const tool = tools.find((candidate) => candidate.name === input.name);\n if (!tool) {\n throw new McpAccessError(404, `Unknown MCP tool: ${input.name}`);\n }\n\n const publicPatterns = principal ? undefined : getPublicPatterns();\n if (!passesBasePolicy(tool, principal, publicPatterns)) {\n throw new McpAccessError(\n 401,\n `Authentication is required for MCP tool: ${input.name}`,\n );\n }\n\n if (!(await passesToolPolicy(tool, principal))) {\n throw new McpAccessError(403, 'MCP tool access is not permitted.', {\n code: MCP_TOOL_ACCESS_DENIED_CODE,\n retryable: false,\n });\n }\n\n const assertion = workflowAssertions[input.name];\n if (assertion) {\n assertion(args, userForGenerator(principal) ?? null);\n }\n\n return makeGenerator(principal).handleToolCall({\n method: 'tools/call',\n params: { arguments: args, name: input.name },\n });\n }\n\n return {\n listTools,\n callTool,\n serverInfo: options.serverInfo,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/server.ts"],"sourcesContent":["/**\n * `createMcpAppServer` returns the framework-agnostic core that backs an\n * app's HTTP MCP endpoints and stdio bridge. It wraps `MCPGenerator` from\n * `@happyvertical/smrt-core` with:\n *\n * - an allow-list of SMRT class names (so apps publish a subset of their\n * objects, not everything decorated with `@smrt()`),\n * - a public-tool policy for unauthenticated callers (read-only patterns\n * via `publicToolPatterns`),\n * - an optional principal-aware `toolPolicy`, used for both discovery and\n * direct calls,\n * - a pluggable `workflowAssertions` hook so apps can guard their own\n * domain-specific tool calls (e.g. \"approval requires an authenticated\n * user\") without that policy living in this package.\n *\n * @packageDocumentation\n */\n\nimport {\n isTenantScopedClassResolved,\n ObjectRegistry,\n} from '@happyvertical/smrt-core';\nimport type {\n MCPConfig,\n MCPResponse,\n MCPTool,\n} from '@happyvertical/smrt-core/generators/mcp';\nimport {\n MCP_STABLE_CATALOG_TTL_MS,\n MCPGenerator,\n} from '@happyvertical/smrt-core/generators/mcp';\nimport { MCP_TOOL_ACCESS_DENIED_CODE, McpAccessError } from './errors.js';\nimport {\n classNamePrefixes,\n compareMcpToolNames,\n isAllowedCoreTool,\n isPublicToolName,\n} from './tools.js';\n\n/**\n * Generic authenticated caller information available to app-MCP policy.\n *\n * `kind`, `roles`, and `scopes` are deliberately unconstrained so an app can\n * represent a human or a scoped service without this package encoding an\n * application's identity or capability model. A missing principal means the\n * request is unauthenticated.\n */\nexport interface McpAppPrincipal {\n id?: string;\n kind?: string;\n roles?: string[];\n scopes?: string[];\n}\n\n/** Minimal legacy user shape used for generated tool-call attribution. */\nexport interface McpAppUser extends McpAppPrincipal {\n id: string;\n}\n\n/** Context supplied to the optional per-tool principal policy. */\nexport interface McpToolPolicyContext {\n principal: McpAppPrincipal | null;\n tool: MCPTool;\n}\n\n/**\n * Per-tool access policy. Return `true` to expose/allow the tool and `false`\n * to hide it from discovery and deny a direct call. A thrown error is treated\n * as a denial so policy implementation details cannot escape the app-MCP\n * boundary.\n */\nexport type McpToolPolicy = (\n context: McpToolPolicyContext,\n) => boolean | Promise<boolean>;\n\n/**\n * Workflow assertion hook signature. Throw `McpAccessError` to reject the\n * call. Implementations may mutate `args` in place to inject server-trusted\n * fields (e.g. clamping `approvedByUserId` to the authenticated user's id).\n */\nexport type McpWorkflowAssertion = (\n args: Record<string, unknown>,\n user: McpAppUser | null,\n) => void;\n\n/**\n * SMRT options thunk — returns the `{ db }` (and similar) bag to pass into\n * MCPGenerator's per-request context. A function is used so apps can lazily\n * resolve env vars at call time.\n */\nexport type McpSmrtOptionsThunk = () => Record<string, unknown>;\n\n/** Public-tool patterns thunk — same lazy-evaluation rationale. */\nexport type McpPublicToolPatternsThunk = () => readonly string[];\n\nexport interface McpToolListCacheHint {\n ttlMs: number;\n cacheScope: 'private' | 'public';\n}\n\nexport interface McpToolListCacheOptions {\n /** Cache lifetime in milliseconds. Defaults to one day for a deploy-static catalog. */\n ttlMs?: number;\n /** Requested cache visibility. Defaults to private. */\n cacheScope?: 'private' | 'public';\n /**\n * Explicit attestation that every allowed tool is global, unauthenticated,\n * and safe to share through an intermediary cache.\n */\n publicCatalog?: true;\n}\n\n/**\n * Options for `createMcpAppServer`.\n */\nexport interface CreateMcpAppServerOptions {\n /** SMRT context bag (db, etc.) passed to MCPGenerator per call. */\n smrtOptions: McpSmrtOptionsThunk;\n /** Server identity surfaced in the MCP protocol. */\n serverInfo: Required<Pick<MCPConfig, 'name' | 'version'>> &\n Pick<MCPConfig, 'description'>;\n /**\n * SMRT class names the app wants to publish. Tools whose name does not\n * start with any of these classes (lowercased + underscore) are filtered\n * out, even if SMRT generated them.\n */\n allowedClassNames: readonly string[];\n /**\n * Optional thunk returning glob-ish patterns for read-only tools that\n * unauthenticated callers are allowed to use. Defaults to an empty list\n * (everything requires auth).\n */\n publicToolPatterns?: McpPublicToolPatternsThunk;\n /**\n * Cache policy for the MCP tools/list result. Public caching is honored only\n * when this explicitly opts in and every allowed tool is a non-tenant,\n * unauthenticated read-only tool with no principal-aware policy.\n */\n toolListCache?: McpToolListCacheOptions;\n /**\n * Optional generic principal-aware tool policy. It is evaluated for every\n * tool that passes the app allow-list and base public/authenticated policy,\n * for both discovery and a direct call.\n */\n toolPolicy?: McpToolPolicy;\n /**\n * Optional per-tool guards. Keyed by tool name. The assertion runs after\n * tool resolution and before `MCPGenerator.handleToolCall`; throwing\n * `McpAccessError` aborts the call with the error's status. Implementations\n * may mutate `args` to inject trusted fields.\n */\n workflowAssertions?: Record<string, McpWorkflowAssertion>;\n}\n\n/** Tool listing inputs. */\nexport interface ListToolsInput {\n /** Caller used for public/authenticated and optional tool-policy checks. */\n principal?: McpAppPrincipal | null;\n /**\n * Backwards-compatible authenticated marker. New mounts should pass\n * `principal` so discovery and direct calls use the same identity.\n */\n authenticated?: boolean;\n}\n\n/** Tool call inputs. */\nexport interface CallToolInput {\n name: string;\n arguments?: Record<string, unknown>;\n /** Caller used for public/authenticated and optional tool-policy checks. */\n principal?: McpAppPrincipal | null;\n /**\n * Backwards-compatible user input. New callers should pass `principal`.\n * Null/undefined means unauthenticated.\n */\n user?: McpAppUser | null;\n}\n\n/** Shape returned by `createMcpAppServer`. */\nexport interface McpAppServer {\n listTools(input: ListToolsInput): Promise<MCPTool[]>;\n callTool(input: CallToolInput): Promise<MCPResponse>;\n /** Cache policy for protocol tools/list responses. */\n getToolsListCacheHint?(): Promise<McpToolListCacheHint>;\n /** Read-only view of the configured server identity. */\n readonly serverInfo: CreateMcpAppServerOptions['serverInfo'];\n}\n\nfunction configuredToolListCacheHint(\n options: McpToolListCacheOptions | undefined,\n): McpToolListCacheHint {\n const ttlMs = options?.ttlMs ?? MCP_STABLE_CATALOG_TTL_MS;\n if (!Number.isSafeInteger(ttlMs) || ttlMs < 0) {\n throw new RangeError(\n 'MCP tools/list cache ttlMs must be a non-negative safe integer.',\n );\n }\n if (\n options?.cacheScope !== undefined &&\n options.cacheScope !== 'private' &&\n options.cacheScope !== 'public'\n ) {\n throw new RangeError(\n \"MCP tools/list cacheScope must be 'private' or 'public'.\",\n );\n }\n return {\n ttlMs,\n cacheScope:\n options?.cacheScope === 'public' && options.publicCatalog === true\n ? 'public'\n : 'private',\n };\n}\n\nfunction isTenantScopedTool(tool: MCPTool): boolean {\n const separator = tool.name.indexOf('_');\n if (separator <= 0) return false;\n const objectName = tool.name.slice(0, separator).toLowerCase();\n for (const [key, classInfo] of ObjectRegistry.getAllClasses()) {\n const name = classInfo.name || key;\n if (name.toLowerCase() === objectName) {\n return (\n ObjectRegistry.isTenantScoped(name) || isTenantScopedClassResolved(name)\n );\n }\n }\n return false;\n}\n\n/**\n * Build the app-runtime MCP server core. The returned object is intentionally\n * framework-agnostic: HTTP wrappers live in `@happyvertical/smrt-app-mcp/sveltekit`\n * and a stdio bridge lives in `@happyvertical/smrt-app-mcp/bin/smrt-mcp-bridge`.\n */\nexport function createMcpAppServer(\n options: CreateMcpAppServerOptions,\n): McpAppServer {\n const allowedPrefixes = classNamePrefixes(options.allowedClassNames);\n const getPublicPatterns =\n options.publicToolPatterns ?? ((): readonly string[] => []);\n const toolPolicy = options.toolPolicy;\n const workflowAssertions = options.workflowAssertions ?? {};\n const requestedToolListCacheHint = configuredToolListCacheHint(\n options.toolListCache,\n );\n\n function userForGenerator(\n principal?: McpAppPrincipal | null,\n ): McpAppUser | undefined {\n if (!principal?.id) return undefined;\n return { id: principal.id, roles: principal.roles };\n }\n\n function makeGenerator(principal?: McpAppPrincipal | null): MCPGenerator {\n const user = userForGenerator(principal);\n return new MCPGenerator(options.serverInfo as MCPConfig, {\n ...options.smrtOptions(),\n user,\n });\n }\n\n function principalForList(input: ListToolsInput): McpAppPrincipal | null {\n if (input.principal !== undefined) return input.principal;\n // Preserve callers of the original boolean API without inventing an id.\n return input.authenticated ? {} : null;\n }\n\n function principalForCall(input: CallToolInput): McpAppPrincipal | null {\n if (input.principal !== undefined) return input.principal;\n return input.user ?? null;\n }\n\n async function allowedTools(): Promise<MCPTool[]> {\n const tools = await makeGenerator().generateTools();\n return tools\n .filter((tool) =>\n isAllowedCoreTool(tool.name.toLowerCase(), allowedPrefixes),\n )\n .sort((left, right) => compareMcpToolNames(left.name, right.name));\n }\n\n function passesBasePolicy(\n tool: MCPTool,\n principal: McpAppPrincipal | null,\n publicPatterns?: readonly string[],\n ): boolean {\n if (principal) return true;\n return isPublicToolName(tool.name, publicPatterns ?? []);\n }\n\n async function passesToolPolicy(\n tool: MCPTool,\n principal: McpAppPrincipal | null,\n ): Promise<boolean> {\n if (!toolPolicy) return true;\n try {\n return Boolean(await toolPolicy({ principal, tool }));\n } catch {\n // A policy failure must fail closed and never leak implementation detail.\n return false;\n }\n }\n\n async function listTools(input: ListToolsInput): Promise<MCPTool[]> {\n const principal = principalForList(input);\n const tools = await allowedTools();\n // Keep the lazy thunk per request, not per tool. Besides avoiding repeated\n // work, this gives one consistent public surface when a thunk reads a\n // dynamic source such as an environment-backed configuration.\n const publicPatterns = principal ? undefined : getPublicPatterns();\n const visible = await Promise.all(\n tools.map(async (tool) => {\n if (!passesBasePolicy(tool, principal, publicPatterns)) return false;\n return passesToolPolicy(tool, principal);\n }),\n );\n return tools.filter((_, index) => visible[index]);\n }\n\n async function getToolsListCacheHint(): Promise<McpToolListCacheHint> {\n if (requestedToolListCacheHint.cacheScope !== 'public') {\n return requestedToolListCacheHint;\n }\n\n // A principal-aware policy can make one caller's catalog differ from\n // another's. Likewise, tenant-scoped reads must never be shared across\n // tenants. The requested public scope is therefore honored only for a\n // complete, unauthenticated, non-tenant read-only catalog.\n const tools = await allowedTools();\n const publicPatterns = getPublicPatterns();\n const isSafePublicCatalog =\n !toolPolicy &&\n tools.every(\n (tool) =>\n isPublicToolName(tool.name, publicPatterns) &&\n !isTenantScopedTool(tool),\n );\n\n return isSafePublicCatalog\n ? requestedToolListCacheHint\n : { ...requestedToolListCacheHint, cacheScope: 'private' };\n }\n\n async function callTool(input: CallToolInput): Promise<MCPResponse> {\n const args = input.arguments ?? {};\n const principal = principalForCall(input);\n const tools = await allowedTools();\n const tool = tools.find((candidate) => candidate.name === input.name);\n if (!tool) {\n throw new McpAccessError(404, 'Unknown MCP tool.');\n }\n\n const publicPatterns = principal ? undefined : getPublicPatterns();\n if (!passesBasePolicy(tool, principal, publicPatterns)) {\n throw new McpAccessError(\n 401,\n `Authentication is required for MCP tool: ${input.name}`,\n );\n }\n\n if (!(await passesToolPolicy(tool, principal))) {\n throw new McpAccessError(403, 'MCP tool access is not permitted.', {\n code: MCP_TOOL_ACCESS_DENIED_CODE,\n retryable: false,\n });\n }\n\n const assertion = workflowAssertions[input.name];\n if (assertion) {\n assertion(args, userForGenerator(principal) ?? null);\n }\n\n return makeGenerator(principal).handleToolCall({\n method: 'tools/call',\n params: { arguments: args, name: input.name },\n });\n }\n\n return {\n listTools,\n callTool,\n getToolsListCacheHint,\n serverInfo: options.serverInfo,\n };\n}\n"],"mappings":"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"}
|
package/dist/manifest.json
CHANGED
package/dist/smrt-knowledge.json
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
4
4
|
"packageName": "@happyvertical/smrt-app-mcp",
|
|
5
|
-
"packageVersion": "0.40.
|
|
5
|
+
"packageVersion": "0.40.61",
|
|
6
6
|
"sourceManifestPath": "dist/manifest.json",
|
|
7
7
|
"agentDocPath": "AGENTS.md",
|
|
8
8
|
"sourceHashes": {
|
|
9
|
-
"manifest": "
|
|
10
|
-
"packageJson": "
|
|
9
|
+
"manifest": "d113e21c3bed1db98a3f3949be2b5ff78ad6cc55e2dcc3353ebd524dbd8b5944",
|
|
10
|
+
"packageJson": "5745b845f7dc79e900206dee5ccb657e90ade1ee43907729194d4da5e5a95cbb",
|
|
11
11
|
"agents": "fd1dc36d1530f81aae49e6efa2e2f5011a8a62d30816f25649d4cb597fc5662a"
|
|
12
12
|
},
|
|
13
13
|
"exports": [
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@happyvertical/smrt-core": "workspace:*",
|
|
19
19
|
"@modelcontextprotocol/server": "2.0.0",
|
|
20
|
+
"@modelcontextprotocol/client": "2.0.0",
|
|
20
21
|
"@modelcontextprotocol/conformance": "0.2.0-alpha.10",
|
|
21
22
|
"@modelcontextprotocol/node": "2.0.0",
|
|
22
23
|
"@types/node": "24.13.2",
|
package/dist/sveltekit.d.ts
CHANGED
|
@@ -35,6 +35,12 @@ declare interface CreateMcpAppServerOptions {
|
|
|
35
35
|
* (everything requires auth).
|
|
36
36
|
*/
|
|
37
37
|
publicToolPatterns?: McpPublicToolPatternsThunk;
|
|
38
|
+
/**
|
|
39
|
+
* Cache policy for the MCP tools/list result. Public caching is honored only
|
|
40
|
+
* when this explicitly opts in and every allowed tool is a non-tenant,
|
|
41
|
+
* unauthenticated read-only tool with no principal-aware policy.
|
|
42
|
+
*/
|
|
43
|
+
toolListCache?: McpToolListCacheOptions;
|
|
38
44
|
/**
|
|
39
45
|
* Optional generic principal-aware tool policy. It is evaluated for every
|
|
40
46
|
* tool that passes the app allow-list and base public/authenticated policy,
|
|
@@ -101,6 +107,8 @@ export declare interface McpAppPrincipal {
|
|
|
101
107
|
export declare interface McpAppServer {
|
|
102
108
|
listTools(input: ListToolsInput): Promise<MCPTool[]>;
|
|
103
109
|
callTool(input: CallToolInput): Promise<MCPResponse>;
|
|
110
|
+
/** Cache policy for protocol tools/list responses. */
|
|
111
|
+
getToolsListCacheHint?(): Promise<McpToolListCacheHint>;
|
|
104
112
|
/** Read-only view of the configured server identity. */
|
|
105
113
|
readonly serverInfo: CreateMcpAppServerOptions['serverInfo'];
|
|
106
114
|
}
|
|
@@ -123,6 +131,26 @@ declare type McpPublicToolPatternsThunk = () => readonly string[];
|
|
|
123
131
|
*/
|
|
124
132
|
declare type McpSmrtOptionsThunk = () => Record<string, unknown>;
|
|
125
133
|
|
|
134
|
+
/** A SvelteKit `+server.ts` request handler with no SvelteKit dependency. */
|
|
135
|
+
export declare type McpSvelteKitHandler = (event: SvelteKitRequestEvent) => Promise<Response>;
|
|
136
|
+
|
|
137
|
+
declare interface McpToolListCacheHint {
|
|
138
|
+
ttlMs: number;
|
|
139
|
+
cacheScope: 'private' | 'public';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
declare interface McpToolListCacheOptions {
|
|
143
|
+
/** Cache lifetime in milliseconds. Defaults to one day for a deploy-static catalog. */
|
|
144
|
+
ttlMs?: number;
|
|
145
|
+
/** Requested cache visibility. Defaults to private. */
|
|
146
|
+
cacheScope?: 'private' | 'public';
|
|
147
|
+
/**
|
|
148
|
+
* Explicit attestation that every allowed tool is global, unauthenticated,
|
|
149
|
+
* and safe to share through an intermediary cache.
|
|
150
|
+
*/
|
|
151
|
+
publicCatalog?: true;
|
|
152
|
+
}
|
|
153
|
+
|
|
126
154
|
/**
|
|
127
155
|
* Per-tool access policy. Return `true` to expose/allow the tool and `false`
|
|
128
156
|
* to hide it from discovery and deny a direct call. A thrown error is treated
|
|
@@ -150,8 +178,20 @@ declare type McpWorkflowAssertion = (args: Record<string, unknown>, user: McpApp
|
|
|
150
178
|
/**
|
|
151
179
|
* Mount `server.callTool` as a `POST` handler that expects
|
|
152
180
|
* `{ name, arguments }` in the JSON body.
|
|
181
|
+
*
|
|
182
|
+
* @deprecated Use {@link mountMcpRoute}; retained for one release so existing
|
|
183
|
+
* REST-shaped mounts can migrate without a coordinated cutover.
|
|
153
184
|
*/
|
|
154
|
-
export declare function mountMcpCallRoute(server: McpAppServer, options?: MountMcpRouteOptions):
|
|
185
|
+
export declare function mountMcpCallRoute(server: McpAppServer, options?: MountMcpRouteOptions): McpSvelteKitHandler;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Mount a modern, stateless Streamable HTTP MCP endpoint as a SvelteKit
|
|
189
|
+
* `POST` handler. The scoped SDK validates the 2026-07-28 envelope plus the
|
|
190
|
+
* required `Mcp-Method` and `Mcp-Name` headers, returning `-32020` on a
|
|
191
|
+
* mismatch. A fresh protocol server is created for each HTTP request, so this
|
|
192
|
+
* route holds neither MCP sessions nor request principal state between nodes.
|
|
193
|
+
*/
|
|
194
|
+
export declare function mountMcpRoute(server: McpAppServer, options?: MountMcpRouteOptions): McpSvelteKitHandler;
|
|
155
195
|
|
|
156
196
|
/** Options shared by both route mounts. */
|
|
157
197
|
export declare interface MountMcpRouteOptions {
|
|
@@ -174,10 +214,11 @@ export declare interface MountMcpRouteOptions {
|
|
|
174
214
|
/**
|
|
175
215
|
* Mount `server.listTools` as a `GET` handler. Returns the tool list shape
|
|
176
216
|
* `{ tools }` for compatibility with the stock MCP bridge.
|
|
217
|
+
*
|
|
218
|
+
* @deprecated Use {@link mountMcpRoute}; retained for one release so existing
|
|
219
|
+
* REST-shaped mounts can migrate without a coordinated cutover.
|
|
177
220
|
*/
|
|
178
|
-
export declare function mountMcpToolsRoute(server: McpAppServer, options?: MountMcpRouteOptions):
|
|
179
|
-
|
|
180
|
-
declare type SvelteKitHandler = (event: SvelteKitRequestEvent) => Promise<Response>;
|
|
221
|
+
export declare function mountMcpToolsRoute(server: McpAppServer, options?: MountMcpRouteOptions): McpSvelteKitHandler;
|
|
181
222
|
|
|
182
223
|
/** Minimal subset of a SvelteKit RequestEvent we actually touch. */
|
|
183
224
|
declare type SvelteKitRequestEvent = {
|
package/dist/sveltekit.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { l as McpAccessError, t as createMcpProtocolServer } from "./chunks/protocol-DfGYbPjN.js";
|
|
2
|
+
import { createMcpHandler } from "@modelcontextprotocol/server";
|
|
2
3
|
//#region src/sveltekit.ts
|
|
3
4
|
var defaultResolvePrincipal = (event) => event.locals?.user ?? null;
|
|
4
5
|
function resolveRequestPrincipal(event, options) {
|
|
@@ -16,6 +17,23 @@ function listToolsInput(resolved) {
|
|
|
16
17
|
if (!resolved.principal && resolved.legacyAuthenticated) return { authenticated: true };
|
|
17
18
|
return { principal: resolved.principal };
|
|
18
19
|
}
|
|
20
|
+
function protocolServerForRequest(server, resolved) {
|
|
21
|
+
if (!resolved.legacyAuthenticated || resolved.principal) return server;
|
|
22
|
+
return {
|
|
23
|
+
serverInfo: server.serverInfo,
|
|
24
|
+
listTools: () => server.listTools({ authenticated: true }),
|
|
25
|
+
callTool: (input) => server.callTool(input)
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function mountMcpRoute(server, options = {}) {
|
|
29
|
+
return async (event) => {
|
|
30
|
+
const resolved = resolveRequestPrincipal(event, options);
|
|
31
|
+
return createMcpHandler(() => createMcpProtocolServer(protocolServerForRequest(server, resolved), { principal: resolved.principal }), {
|
|
32
|
+
legacy: "reject",
|
|
33
|
+
maxSubscriptions: 0
|
|
34
|
+
}).fetch(event.request);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
19
37
|
function mountMcpToolsRoute(server, options = {}) {
|
|
20
38
|
return async (event) => {
|
|
21
39
|
try {
|
|
@@ -61,6 +79,6 @@ function jsonResponse(body, status = 200) {
|
|
|
61
79
|
});
|
|
62
80
|
}
|
|
63
81
|
//#endregion
|
|
64
|
-
export { McpAccessError, mountMcpCallRoute, mountMcpToolsRoute };
|
|
82
|
+
export { McpAccessError, mountMcpCallRoute, mountMcpRoute, mountMcpToolsRoute };
|
|
65
83
|
|
|
66
84
|
//# sourceMappingURL=sveltekit.js.map
|
package/dist/sveltekit.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sveltekit.js","names":[],"sources":["../src/sveltekit.ts"],"sourcesContent":["/**\n * SvelteKit route adapters for an `McpAppServer`. Mirrors the\n * `@happyvertical/smrt-users/sveltekit` pattern: minimal `HandleInput` type\n * so we never need `@sveltejs/kit` as a real dependency.\n *\n * @packageDocumentation\n *\n * @example\n * ```ts\n * // src/routes/api/mcp/tools/+server.ts\n * import { mountMcpToolsRoute } from '@happyvertical/smrt-app-mcp/sveltekit';\n * import { mcpServer } from '$lib/server/mcp';\n * export const GET = mountMcpToolsRoute(mcpServer);\n * ```\n */\n\nimport { McpAccessError } from './errors.js';\nimport type { CallToolInput, McpAppPrincipal, McpAppServer } from './server.js';\n\n/** Minimal subset of a SvelteKit RequestEvent we actually touch. */\ntype SvelteKitRequestEvent = {\n locals?: Record<string, unknown>;\n request: Request;\n url: URL;\n};\n\ntype SvelteKitHandler = (event: SvelteKitRequestEvent) => Promise<Response>;\n\ntype ResolvedRequestPrincipal = {\n principal: McpAppPrincipal | null;\n /** Defined only for the legacy discovery-only authentication adapter. */\n legacyAuthenticated?: boolean;\n};\n\n/** Locals reader used to pull the request principal out of `event.locals`. */\nexport type McpPrincipalResolver = (\n event: SvelteKitRequestEvent,\n) => McpAppPrincipal | null | undefined;\n\n/** Backwards-compatible alias for callers that name the principal a user. */\nexport type McpUserResolver = McpPrincipalResolver;\n\nconst defaultResolvePrincipal: McpPrincipalResolver = (event) =>\n (event.locals?.user ?? null) as McpAppPrincipal | null;\n\nfunction resolveRequestPrincipal(\n event: SvelteKitRequestEvent,\n options: MountMcpRouteOptions,\n): ResolvedRequestPrincipal {\n const resolvePrincipal =\n options.resolvePrincipal ?? options.resolveUser ?? defaultResolvePrincipal;\n const principal = resolvePrincipal(event) ?? null;\n // Keep the legacy boolean gate for existing apps, but apply it to the same\n // principal that both routes receive. A new principal resolver supersedes it.\n if (\n !options.resolvePrincipal &&\n options.resolveAuthenticated &&\n !options.resolveAuthenticated(event)\n ) {\n return { principal: null, legacyAuthenticated: false };\n }\n return {\n principal,\n ...(options.resolvePrincipal || !options.resolveAuthenticated\n ? {}\n : { legacyAuthenticated: true }),\n };\n}\n\nfunction listToolsInput(resolved: ResolvedRequestPrincipal) {\n // Before principal-aware routes, `resolveAuthenticated` affected discovery\n // independently of `resolveUser`. Preserve a true legacy result only when\n // there is no principal to pass; new routes should use `resolvePrincipal`\n // for a single identity on both discovery and calls.\n if (!resolved.principal && resolved.legacyAuthenticated) {\n return { authenticated: true };\n }\n return { principal: resolved.principal };\n}\n\n/** Options shared by both route mounts. */\nexport interface MountMcpRouteOptions {\n /**\n * Resolve the request principal once for both discovery and direct calls.\n * Defaults to `event.locals.user`.\n */\n resolvePrincipal?: McpPrincipalResolver;\n /**\n * Backwards-compatible alias for `resolvePrincipal`.\n */\n resolveUser?: McpUserResolver;\n /**\n * Deprecated legacy authentication gate. When `resolvePrincipal` is not\n * supplied, a false result makes the principal null for both routes.\n */\n resolveAuthenticated?: (event: SvelteKitRequestEvent) => boolean;\n}\n\n/**\n * Mount `server.listTools` as a `GET` handler. Returns the tool list shape\n * `{ tools }` for compatibility with the stock MCP bridge.\n */\nexport function mountMcpToolsRoute(\n server: McpAppServer,\n options: MountMcpRouteOptions = {},\n): SvelteKitHandler {\n return async (event) => {\n try {\n const tools = await server.listTools(\n listToolsInput(resolveRequestPrincipal(event, options)),\n );\n return jsonResponse({ tools });\n } catch (error) {\n if (error instanceof McpAccessError) {\n return jsonResponse(mcpAccessErrorBody(error), error.status);\n }\n throw error;\n }\n };\n}\n\n/**\n * Mount `server.callTool` as a `POST` handler that expects\n * `{ name, arguments }` in the JSON body.\n */\nexport function mountMcpCallRoute(\n server: McpAppServer,\n options: MountMcpRouteOptions = {},\n): SvelteKitHandler {\n return async (event) => {\n const body = (await event.request.json().catch(() => null)) as {\n arguments?: Record<string, unknown>;\n name?: string;\n } | null;\n\n if (!body?.name) {\n return jsonResponse({ error: 'name is required.' }, 400);\n }\n\n const input: CallToolInput = {\n arguments: body.arguments ?? {},\n name: body.name,\n principal: resolveRequestPrincipal(event, options).principal,\n };\n\n try {\n return jsonResponse(await server.callTool(input));\n } catch (error) {\n if (error instanceof McpAccessError) {\n return jsonResponse(mcpAccessErrorBody(error), error.status);\n }\n throw error;\n }\n };\n}\n\nfunction mcpAccessErrorBody(error: McpAccessError): unknown {\n const { code, retryable } = error.metadata;\n // Preserve the legacy shape unless an error deliberately opts into the\n // shared structured failure contract.\n if (!code) return { error: error.message };\n return {\n error: {\n ok: false,\n code,\n message: error.message,\n status: error.status,\n ...(retryable === undefined ? {} : { retryable }),\n },\n };\n}\n\nfunction jsonResponse(body: unknown, status = 200): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { 'content-type': 'application/json' },\n });\n}\n\nexport { McpAccessError } from './errors.js';\nexport type { McpAppPrincipal, McpAppServer } from './server.js';\n"],"mappings":";;AA0CA,IAAM,2BAAiD,UACpD,MAAM,QAAQ,QAAQ;AAEzB,SAAS,wBACP,OACA,SAC0B;CAG1B,MAAM,aADJ,QAAQ,oBAAoB,QAAQ,eAAe,wBAAA,CAClB,KAAK,KAAK;CAG7C,IACE,CAAC,QAAQ,oBACT,QAAQ,wBACR,CAAC,QAAQ,qBAAqB,KAAK,GAEnC,OAAO;EAAE,WAAW;EAAM,qBAAqB;CAAM;CAEvD,OAAO;EACL;EACA,GAAI,QAAQ,oBAAoB,CAAC,QAAQ,uBACrC,CAAC,IACD,EAAE,qBAAqB,KAAK;CAClC;AACF;AAEA,SAAS,eAAe,UAAoC;CAK1D,IAAI,CAAC,SAAS,aAAa,SAAS,qBAClC,OAAO,EAAE,eAAe,KAAK;CAE/B,OAAO,EAAE,WAAW,SAAS,UAAU;AACzC;AAwBO,SAAS,mBACd,QACA,UAAgC,CAAC,GACf;CAClB,OAAO,OAAO,UAAU;EACtB,IAAI;GAIF,OAAO,aAAa,EAAE,OAAA,MAHF,OAAO,UACzB,eAAe,wBAAwB,OAAO,OAAO,CAAC,CACxD,EAC4B,CAAC;EAC/B,SAAS,OAAO;GACd,IAAI,iBAAiB,gBACnB,OAAO,aAAa,mBAAmB,KAAK,GAAG,MAAM,MAAM;GAE7D,MAAM;EACR;CACF;AACF;AAMO,SAAS,kBACd,QACA,UAAgC,CAAC,GACf;CAClB,OAAO,OAAO,UAAU;EACtB,MAAM,OAAQ,MAAM,MAAM,QAAQ,KAAK,CAAA,CAAE,YAAY,IAAI;EAKzD,IAAI,CAAC,MAAM,MACT,OAAO,aAAa,EAAE,OAAO,oBAAoB,GAAG,GAAG;EAGzD,MAAM,QAAuB;GAC3B,WAAW,KAAK,aAAa,CAAC;GAC9B,MAAM,KAAK;GACX,WAAW,wBAAwB,OAAO,OAAO,CAAA,CAAE;EACrD;EAEA,IAAI;GACF,OAAO,aAAa,MAAM,OAAO,SAAS,KAAK,CAAC;EAClD,SAAS,OAAO;GACd,IAAI,iBAAiB,gBACnB,OAAO,aAAa,mBAAmB,KAAK,GAAG,MAAM,MAAM;GAE7D,MAAM;EACR;CACF;AACF;AAEA,SAAS,mBAAmB,OAAgC;CAC1D,MAAM,EAAE,MAAM,cAAc,MAAM;CAGlC,IAAI,CAAC,MAAM,OAAO,EAAE,OAAO,MAAM,QAAQ;CACzC,OAAO,EACL,OAAO;EACL,IAAI;EACJ;EACA,SAAS,MAAM;EACf,QAAQ,MAAM;EACd,GAAI,cAAc,KAAA,IAAY,CAAC,IAAI,EAAE,UAAU;CACjD,EACF;AACF;AAEA,SAAS,aAAa,MAAe,SAAS,KAAe;CAC3D,OAAO,IAAI,SAAS,KAAK,UAAU,IAAI,GAAG;EACxC;EACA,SAAS,EAAE,gBAAgB,mBAAmB;CAChD,CAAC;AACH"}
|
|
1
|
+
{"version":3,"file":"sveltekit.js","names":[],"sources":["../src/sveltekit.ts"],"sourcesContent":["/**\n * SvelteKit route adapters for an `McpAppServer`. Mirrors the\n * `@happyvertical/smrt-users/sveltekit` pattern: minimal `HandleInput` type\n * so we never need `@sveltejs/kit` as a real dependency.\n *\n * @packageDocumentation\n *\n * @example\n * ```ts\n * // src/routes/api/mcp/+server.ts\n * import { mountMcpRoute } from '@happyvertical/smrt-app-mcp/sveltekit';\n * import { mcpServer } from '$lib/server/mcp';\n * export const POST = mountMcpRoute(mcpServer);\n * ```\n */\n\nimport { createMcpHandler } from '@modelcontextprotocol/server';\nimport { McpAccessError } from './errors.js';\nimport { createMcpProtocolServer } from './protocol.js';\nimport type { CallToolInput, McpAppPrincipal, McpAppServer } from './server.js';\n\n/** Minimal subset of a SvelteKit RequestEvent we actually touch. */\ntype SvelteKitRequestEvent = {\n locals?: Record<string, unknown>;\n request: Request;\n url: URL;\n};\n\n/** A SvelteKit `+server.ts` request handler with no SvelteKit dependency. */\nexport type McpSvelteKitHandler = (\n event: SvelteKitRequestEvent,\n) => Promise<Response>;\n\ntype ResolvedRequestPrincipal = {\n principal: McpAppPrincipal | null;\n /** Defined only for the legacy discovery-only authentication adapter. */\n legacyAuthenticated?: boolean;\n};\n\n/** Locals reader used to pull the request principal out of `event.locals`. */\nexport type McpPrincipalResolver = (\n event: SvelteKitRequestEvent,\n) => McpAppPrincipal | null | undefined;\n\n/** Backwards-compatible alias for callers that name the principal a user. */\nexport type McpUserResolver = McpPrincipalResolver;\n\nconst defaultResolvePrincipal: McpPrincipalResolver = (event) =>\n (event.locals?.user ?? null) as McpAppPrincipal | null;\n\nfunction resolveRequestPrincipal(\n event: SvelteKitRequestEvent,\n options: MountMcpRouteOptions,\n): ResolvedRequestPrincipal {\n const resolvePrincipal =\n options.resolvePrincipal ?? options.resolveUser ?? defaultResolvePrincipal;\n const principal = resolvePrincipal(event) ?? null;\n // Keep the legacy boolean gate for existing apps, but apply it to the same\n // principal that both routes receive. A new principal resolver supersedes it.\n if (\n !options.resolvePrincipal &&\n options.resolveAuthenticated &&\n !options.resolveAuthenticated(event)\n ) {\n return { principal: null, legacyAuthenticated: false };\n }\n return {\n principal,\n ...(options.resolvePrincipal || !options.resolveAuthenticated\n ? {}\n : { legacyAuthenticated: true }),\n };\n}\n\nfunction listToolsInput(resolved: ResolvedRequestPrincipal) {\n // Before principal-aware routes, `resolveAuthenticated` affected discovery\n // independently of `resolveUser`. Preserve a true legacy result only when\n // there is no principal to pass; new routes should use `resolvePrincipal`\n // for a single identity on both discovery and calls.\n if (!resolved.principal && resolved.legacyAuthenticated) {\n return { authenticated: true };\n }\n return { principal: resolved.principal };\n}\n\n/** Options shared by both route mounts. */\nexport interface MountMcpRouteOptions {\n /**\n * Resolve the request principal once for both discovery and direct calls.\n * Defaults to `event.locals.user`.\n */\n resolvePrincipal?: McpPrincipalResolver;\n /**\n * Backwards-compatible alias for `resolvePrincipal`.\n */\n resolveUser?: McpUserResolver;\n /**\n * Deprecated legacy authentication gate. When `resolvePrincipal` is not\n * supplied, a false result makes the principal null for both routes.\n */\n resolveAuthenticated?: (event: SvelteKitRequestEvent) => boolean;\n}\n\nfunction protocolServerForRequest(\n server: McpAppServer,\n resolved: ResolvedRequestPrincipal,\n): McpAppServer {\n // Older applications sometimes used a boolean discovery-only adapter. Keep\n // its positive result intact for the deprecated resolver while new mounts\n // consistently use the principal on both MCP methods.\n if (!resolved.legacyAuthenticated || resolved.principal) return server;\n return {\n serverInfo: server.serverInfo,\n listTools: () => server.listTools({ authenticated: true }),\n callTool: (input) => server.callTool(input),\n };\n}\n\n/**\n * Mount a modern, stateless Streamable HTTP MCP endpoint as a SvelteKit\n * `POST` handler. The scoped SDK validates the 2026-07-28 envelope plus the\n * required `Mcp-Method` and `Mcp-Name` headers, returning `-32020` on a\n * mismatch. A fresh protocol server is created for each HTTP request, so this\n * route holds neither MCP sessions nor request principal state between nodes.\n */\nexport function mountMcpRoute(\n server: McpAppServer,\n options: MountMcpRouteOptions = {},\n): McpSvelteKitHandler {\n return async (event) => {\n const resolved = resolveRequestPrincipal(event, options);\n const handler = createMcpHandler(\n () =>\n createMcpProtocolServer(protocolServerForRequest(server, resolved), {\n principal: resolved.principal,\n }),\n {\n // The legacy REST-shaped mounts below remain the migration path for\n // one release. This endpoint is deliberately 2026-07-28-only.\n legacy: 'reject',\n // The SDK validates every request before consulting its listen router.\n // Zero capacity keeps this tools-only mount stateless by refusing a\n // listen request before it can open an SSE response.\n maxSubscriptions: 0,\n },\n );\n return handler.fetch(event.request);\n };\n}\n\n/**\n * Mount `server.listTools` as a `GET` handler. Returns the tool list shape\n * `{ tools }` for compatibility with the stock MCP bridge.\n *\n * @deprecated Use {@link mountMcpRoute}; retained for one release so existing\n * REST-shaped mounts can migrate without a coordinated cutover.\n */\nexport function mountMcpToolsRoute(\n server: McpAppServer,\n options: MountMcpRouteOptions = {},\n): McpSvelteKitHandler {\n return async (event) => {\n try {\n const tools = await server.listTools(\n listToolsInput(resolveRequestPrincipal(event, options)),\n );\n return jsonResponse({ tools });\n } catch (error) {\n if (error instanceof McpAccessError) {\n return jsonResponse(mcpAccessErrorBody(error), error.status);\n }\n throw error;\n }\n };\n}\n\n/**\n * Mount `server.callTool` as a `POST` handler that expects\n * `{ name, arguments }` in the JSON body.\n *\n * @deprecated Use {@link mountMcpRoute}; retained for one release so existing\n * REST-shaped mounts can migrate without a coordinated cutover.\n */\nexport function mountMcpCallRoute(\n server: McpAppServer,\n options: MountMcpRouteOptions = {},\n): McpSvelteKitHandler {\n return async (event) => {\n const body = (await event.request.json().catch(() => null)) as {\n arguments?: Record<string, unknown>;\n name?: string;\n } | null;\n\n if (!body?.name) {\n return jsonResponse({ error: 'name is required.' }, 400);\n }\n\n const input: CallToolInput = {\n arguments: body.arguments ?? {},\n name: body.name,\n principal: resolveRequestPrincipal(event, options).principal,\n };\n\n try {\n return jsonResponse(await server.callTool(input));\n } catch (error) {\n if (error instanceof McpAccessError) {\n return jsonResponse(mcpAccessErrorBody(error), error.status);\n }\n throw error;\n }\n };\n}\n\nfunction mcpAccessErrorBody(error: McpAccessError): unknown {\n const { code, retryable } = error.metadata;\n // Preserve the legacy shape unless an error deliberately opts into the\n // shared structured failure contract.\n if (!code) return { error: error.message };\n return {\n error: {\n ok: false,\n code,\n message: error.message,\n status: error.status,\n ...(retryable === undefined ? {} : { retryable }),\n },\n };\n}\n\nfunction jsonResponse(body: unknown, status = 200): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { 'content-type': 'application/json' },\n });\n}\n\nexport { McpAccessError } from './errors.js';\nexport type { McpAppPrincipal, McpAppServer } from './server.js';\n"],"mappings":";;;AA+CA,IAAM,2BAAiD,UACpD,MAAM,QAAQ,QAAQ;AAEzB,SAAS,wBACP,OACA,SAC0B;CAG1B,MAAM,aADJ,QAAQ,oBAAoB,QAAQ,eAAe,wBAAA,CAClB,KAAK,KAAK;CAG7C,IACE,CAAC,QAAQ,oBACT,QAAQ,wBACR,CAAC,QAAQ,qBAAqB,KAAK,GAEnC,OAAO;EAAE,WAAW;EAAM,qBAAqB;CAAM;CAEvD,OAAO;EACL;EACA,GAAI,QAAQ,oBAAoB,CAAC,QAAQ,uBACrC,CAAC,IACD,EAAE,qBAAqB,KAAK;CAClC;AACF;AAEA,SAAS,eAAe,UAAoC;CAK1D,IAAI,CAAC,SAAS,aAAa,SAAS,qBAClC,OAAO,EAAE,eAAe,KAAK;CAE/B,OAAO,EAAE,WAAW,SAAS,UAAU;AACzC;AAoBA,SAAS,yBACP,QACA,UACc;CAId,IAAI,CAAC,SAAS,uBAAuB,SAAS,WAAW,OAAO;CAChE,OAAO;EACL,YAAY,OAAO;EACnB,iBAAiB,OAAO,UAAU,EAAE,eAAe,KAAK,CAAC;EACzD,WAAW,UAAU,OAAO,SAAS,KAAK;CAC5C;AACF;AASO,SAAS,cACd,QACA,UAAgC,CAAC,GACZ;CACrB,OAAO,OAAO,UAAU;EACtB,MAAM,WAAW,wBAAwB,OAAO,OAAO;EAgBvD,OAfgB,uBAEZ,wBAAwB,yBAAyB,QAAQ,QAAQ,GAAG,EAClE,WAAW,SAAS,UACtB,CAAC,GACH;GAGE,QAAQ;GAIR,kBAAkB;EACpB,CAEK,CAAA,CAAQ,MAAM,MAAM,OAAO;CACpC;AACF;AASO,SAAS,mBACd,QACA,UAAgC,CAAC,GACZ;CACrB,OAAO,OAAO,UAAU;EACtB,IAAI;GAIF,OAAO,aAAa,EAAE,OAAA,MAHF,OAAO,UACzB,eAAe,wBAAwB,OAAO,OAAO,CAAC,CACxD,EAC4B,CAAC;EAC/B,SAAS,OAAO;GACd,IAAI,iBAAiB,gBACnB,OAAO,aAAa,mBAAmB,KAAK,GAAG,MAAM,MAAM;GAE7D,MAAM;EACR;CACF;AACF;AASO,SAAS,kBACd,QACA,UAAgC,CAAC,GACZ;CACrB,OAAO,OAAO,UAAU;EACtB,MAAM,OAAQ,MAAM,MAAM,QAAQ,KAAK,CAAA,CAAE,YAAY,IAAI;EAKzD,IAAI,CAAC,MAAM,MACT,OAAO,aAAa,EAAE,OAAO,oBAAoB,GAAG,GAAG;EAGzD,MAAM,QAAuB;GAC3B,WAAW,KAAK,aAAa,CAAC;GAC9B,MAAM,KAAK;GACX,WAAW,wBAAwB,OAAO,OAAO,CAAA,CAAE;EACrD;EAEA,IAAI;GACF,OAAO,aAAa,MAAM,OAAO,SAAS,KAAK,CAAC;EAClD,SAAS,OAAO;GACd,IAAI,iBAAiB,gBACnB,OAAO,aAAa,mBAAmB,KAAK,GAAG,MAAM,MAAM;GAE7D,MAAM;EACR;CACF;AACF;AAEA,SAAS,mBAAmB,OAAgC;CAC1D,MAAM,EAAE,MAAM,cAAc,MAAM;CAGlC,IAAI,CAAC,MAAM,OAAO,EAAE,OAAO,MAAM,QAAQ;CACzC,OAAO,EACL,OAAO;EACL,IAAI;EACJ;EACA,SAAS,MAAM;EACf,QAAQ,MAAM;EACd,GAAI,cAAc,KAAA,IAAY,CAAC,IAAI,EAAE,UAAU;CACjD,EACF;AACF;AAEA,SAAS,aAAa,MAAe,SAAS,KAAe;CAC3D,OAAO,IAAI,SAAS,KAAK,UAAU,IAAI,GAAG;EACxC;EACA,SAAS,EAAE,gBAAgB,mBAAmB;CAChD,CAAC;AACH"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-app-mcp",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.61",
|
|
4
4
|
"description": "App-runtime MCP server scaffolding for SMRT apps — `createMcpAppServer` plus transport adapters (SvelteKit today) for exposing a SMRT app's MCP surface over HTTP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -44,9 +44,10 @@
|
|
|
44
44
|
"author": "HappyVertical",
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@modelcontextprotocol/server": "2.0.0",
|
|
47
|
-
"@happyvertical/smrt-core": "0.40.
|
|
47
|
+
"@happyvertical/smrt-core": "0.40.61"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
+
"@modelcontextprotocol/client": "2.0.0",
|
|
50
51
|
"@modelcontextprotocol/conformance": "0.2.0-alpha.10",
|
|
51
52
|
"@modelcontextprotocol/node": "2.0.0",
|
|
52
53
|
"@types/node": "24.13.2",
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
//#region src/errors.ts
|
|
2
|
-
var MCP_TOOL_ACCESS_DENIED_CODE = "mcp_tool_access_denied";
|
|
3
|
-
var McpAccessError = class extends Error {
|
|
4
|
-
constructor(status, message, metadata = {}) {
|
|
5
|
-
super(message);
|
|
6
|
-
this.status = status;
|
|
7
|
-
this.metadata = metadata;
|
|
8
|
-
this.name = "McpAccessError";
|
|
9
|
-
}
|
|
10
|
-
status;
|
|
11
|
-
metadata;
|
|
12
|
-
};
|
|
13
|
-
//#endregion
|
|
14
|
-
export { McpAccessError as n, MCP_TOOL_ACCESS_DENIED_CODE as t };
|
|
15
|
-
|
|
16
|
-
//# sourceMappingURL=errors-CHYu0Vr2.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"errors-CHYu0Vr2.js","names":[],"sources":["../../src/errors.ts"],"sourcesContent":["/** Machine-readable code for a principal policy denial. */\nexport const MCP_TOOL_ACCESS_DENIED_CODE = 'mcp_tool_access_denied';\n\n/**\n * Metadata that is safe to expose for an app-MCP access failure. Policy\n * implementations must not place principal, scope, tool, or internal-error\n * details here.\n */\nexport interface McpAccessErrorMetadata {\n code?: string;\n retryable?: boolean;\n}\n\n/**\n * Error returned by the MCP app server when a caller tries to use a tool\n * they are not allowed to access. The HTTP layer should map `status` onto\n * the response status code.\n */\nexport class McpAccessError extends Error {\n constructor(\n readonly status: number,\n message: string,\n readonly metadata: McpAccessErrorMetadata = {},\n ) {\n super(message);\n this.name = 'McpAccessError';\n }\n}\n"],"mappings":";AACO,IAAM,8BAA8B;AAiBpC,IAAM,iBAAN,cAA6B,MAAM;CACxC,YACW,QACT,SACS,WAAmC,CAAC,GAC7C;EACA,MAAM,OAAO;EAJJ,KAAA,SAAA;EAEA,KAAA,WAAA;EAGT,KAAK,OAAO;CACd;CANW;CAEA;AAKb"}
|