@ggui-ai/mcp-server 0.1.0-rc.1
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/LICENSE +201 -0
- package/README.md +48 -0
- package/dist/admin-blueprints-transport.d.ts +114 -0
- package/dist/admin-blueprints-transport.d.ts.map +1 -0
- package/dist/admin-blueprints-transport.js +118 -0
- package/dist/admin-oauth-providers-transport.d.ts +40 -0
- package/dist/admin-oauth-providers-transport.d.ts.map +1 -0
- package/dist/admin-oauth-providers-transport.js +263 -0
- package/dist/auth.d.ts +39 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +75 -0
- package/dist/build-mcp.d.ts +128 -0
- package/dist/build-mcp.d.ts.map +1 -0
- package/dist/build-mcp.js +113 -0
- package/dist/code-store-fs.d.ts +19 -0
- package/dist/code-store-fs.d.ts.map +1 -0
- package/dist/code-store-fs.js +98 -0
- package/dist/console-auth.d.ts +139 -0
- package/dist/console-auth.d.ts.map +1 -0
- package/dist/console-auth.js +102 -0
- package/dist/console-cache.d.ts +78 -0
- package/dist/console-cache.d.ts.map +1 -0
- package/dist/console-cache.js +105 -0
- package/dist/console-headers.d.ts +124 -0
- package/dist/console-headers.d.ts.map +1 -0
- package/dist/console-headers.js +49 -0
- package/dist/console-llm-trace.d.ts +66 -0
- package/dist/console-llm-trace.d.ts.map +1 -0
- package/dist/console-llm-trace.js +105 -0
- package/dist/console-payloads.d.ts +67 -0
- package/dist/console-payloads.d.ts.map +1 -0
- package/dist/console-payloads.js +105 -0
- package/dist/console-theme-routes.d.ts +111 -0
- package/dist/console-theme-routes.d.ts.map +1 -0
- package/dist/console-theme-routes.js +202 -0
- package/dist/console-timeline.d.ts +45 -0
- package/dist/console-timeline.d.ts.map +1 -0
- package/dist/console-timeline.js +169 -0
- package/dist/console-validator.d.ts +67 -0
- package/dist/console-validator.d.ts.map +1 -0
- package/dist/console-validator.js +105 -0
- package/dist/console-welcome.d.ts +7 -0
- package/dist/console-welcome.d.ts.map +1 -0
- package/dist/console-welcome.js +221 -0
- package/dist/csrf-middleware.d.ts +55 -0
- package/dist/csrf-middleware.d.ts.map +1 -0
- package/dist/csrf-middleware.js +138 -0
- package/dist/email-login.d.ts +174 -0
- package/dist/email-login.d.ts.map +1 -0
- package/dist/email-login.js +254 -0
- package/dist/email-resend.d.ts +29 -0
- package/dist/email-resend.d.ts.map +1 -0
- package/dist/email-resend.js +71 -0
- package/dist/email-sender-from-env.d.ts +34 -0
- package/dist/email-sender-from-env.d.ts.map +1 -0
- package/dist/email-sender-from-env.js +112 -0
- package/dist/email-smtp.d.ts +42 -0
- package/dist/email-smtp.d.ts.map +1 -0
- package/dist/email-smtp.js +81 -0
- package/dist/index.d.ts +102 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +122 -0
- package/dist/instructions-presets.d.ts +112 -0
- package/dist/instructions-presets.d.ts.map +1 -0
- package/dist/instructions-presets.js +195 -0
- package/dist/llm-backed-negotiator.d.ts +178 -0
- package/dist/llm-backed-negotiator.d.ts.map +1 -0
- package/dist/llm-backed-negotiator.js +579 -0
- package/dist/logger.d.ts +23 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +41 -0
- package/dist/mcp-apps-inbound.d.ts +86 -0
- package/dist/mcp-apps-inbound.d.ts.map +1 -0
- package/dist/mcp-apps-inbound.js +278 -0
- package/dist/mcp-apps-outbound.d.ts +448 -0
- package/dist/mcp-apps-outbound.d.ts.map +1 -0
- package/dist/mcp-apps-outbound.js +1163 -0
- package/dist/mcp-mounts.d.ts +239 -0
- package/dist/mcp-mounts.d.ts.map +1 -0
- package/dist/mcp-mounts.js +222 -0
- package/dist/oauth-login-types.d.ts +160 -0
- package/dist/oauth-login-types.d.ts.map +1 -0
- package/dist/oauth-login-types.js +9 -0
- package/dist/oauth-login.d.ts +77 -0
- package/dist/oauth-login.d.ts.map +1 -0
- package/dist/oauth-login.js +455 -0
- package/dist/oauth-providers/github.d.ts +17 -0
- package/dist/oauth-providers/github.d.ts.map +1 -0
- package/dist/oauth-providers/github.js +89 -0
- package/dist/oauth-providers/google.d.ts +18 -0
- package/dist/oauth-providers/google.d.ts.map +1 -0
- package/dist/oauth-providers/google.js +59 -0
- package/dist/oauth-providers-store.d.ts +32 -0
- package/dist/oauth-providers-store.d.ts.map +1 -0
- package/dist/oauth-providers-store.js +291 -0
- package/dist/oauth.d.ts +347 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +686 -0
- package/dist/pairing-transport.d.ts +99 -0
- package/dist/pairing-transport.d.ts.map +1 -0
- package/dist/pairing-transport.js +223 -0
- package/dist/rate-limit-middleware.d.ts +36 -0
- package/dist/rate-limit-middleware.d.ts.map +1 -0
- package/dist/rate-limit-middleware.js +57 -0
- package/dist/render-gate.d.ts +87 -0
- package/dist/render-gate.d.ts.map +1 -0
- package/dist/render-gate.js +77 -0
- package/dist/render-rate-limit.d.ts +59 -0
- package/dist/render-rate-limit.d.ts.map +1 -0
- package/dist/render-rate-limit.js +73 -0
- package/dist/render-signing.d.ts +98 -0
- package/dist/render-signing.d.ts.map +1 -0
- package/dist/render-signing.js +113 -0
- package/dist/request-context.d.ts +113 -0
- package/dist/request-context.d.ts.map +1 -0
- package/dist/request-context.js +154 -0
- package/dist/reserved-validators.d.ts +22 -0
- package/dist/reserved-validators.d.ts.map +1 -0
- package/dist/reserved-validators.js +101 -0
- package/dist/schema-compat.d.ts +167 -0
- package/dist/schema-compat.d.ts.map +1 -0
- package/dist/schema-compat.js +187 -0
- package/dist/security-headers-middleware.d.ts +38 -0
- package/dist/security-headers-middleware.d.ts.map +1 -0
- package/dist/security-headers-middleware.js +30 -0
- package/dist/server.d.ts +2060 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +6338 -0
- package/dist/session-channel.d.ts +651 -0
- package/dist/session-channel.d.ts.map +1 -0
- package/dist/session-channel.js +1756 -0
- package/dist/storage.d.ts +89 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +171 -0
- package/dist/thread-transport.d.ts +118 -0
- package/dist/thread-transport.d.ts.map +1 -0
- package/dist/thread-transport.js +478 -0
- package/dist/user-session-auth.d.ts +167 -0
- package/dist/user-session-auth.d.ts.map +1 -0
- package/dist/user-session-auth.js +148 -0
- package/package.json +76 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Apps inbound hosting — resource proxy + tools/call visibility gate.
|
|
3
|
+
*
|
|
4
|
+
* When the ggui server hosts a third-party MCP App iframe, the browser
|
|
5
|
+
* CANNOT resolve `ui://` URIs directly (browser URL scheme set is
|
|
6
|
+
* limited to http/https/etc). The server owns the only path to the
|
|
7
|
+
* resource bytes:
|
|
8
|
+
*
|
|
9
|
+
* browser iframe
|
|
10
|
+
* ↓ https://<ggui-server>/mcp-apps/resource?session=…&item=…
|
|
11
|
+
* ggui-server proxy route
|
|
12
|
+
* ↓ MCP resources/read (via @modelcontextprotocol/sdk/client)
|
|
13
|
+
* source MCP server
|
|
14
|
+
* ↓ ui:// resource HTML
|
|
15
|
+
* ggui-server returns the HTML bytes to the iframe.
|
|
16
|
+
*
|
|
17
|
+
* Similarly, iframe-originated `tools/call` flows:
|
|
18
|
+
*
|
|
19
|
+
* iframe ----postMessage---> ggui client (in parent)
|
|
20
|
+
* ggui client -----fetch----> ggui-server /mcp-apps/tools-call
|
|
21
|
+
* ggui-server enforces visibility, proxies via MCP client to source.
|
|
22
|
+
*
|
|
23
|
+
* Visibility enforcement lives at the proxy boundary (this file), NOT
|
|
24
|
+
* in the client. Source `tools/list` is fetched + cached per connector;
|
|
25
|
+
* `_meta.ui.visibility` gates which tools iframe-originated calls can
|
|
26
|
+
* invoke. Cross-connector calls are rejected — the iframe for connector
|
|
27
|
+
* A can NEVER call tools on connector B.
|
|
28
|
+
*/
|
|
29
|
+
import type { Express } from 'express';
|
|
30
|
+
import { type ConnectorRegistry, type SessionStore } from '@ggui-ai/mcp-server-core';
|
|
31
|
+
import type { Logger } from './logger.js';
|
|
32
|
+
/** Default mount path for the MCP Apps inbound routes. */
|
|
33
|
+
export declare const DEFAULT_MCP_APPS_INBOUND_PATH = "/mcp-apps";
|
|
34
|
+
export interface McpAppsInboundOptions {
|
|
35
|
+
readonly connectors: ConnectorRegistry;
|
|
36
|
+
readonly sessionStore: SessionStore;
|
|
37
|
+
readonly logger: Logger;
|
|
38
|
+
/** Override for the mount path prefix. Defaults to `/mcp-apps`. */
|
|
39
|
+
readonly path?: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Shape of one entry in a cached source `tools/list`.
|
|
43
|
+
*
|
|
44
|
+
* Kept local — we only care about the fields that gate visibility,
|
|
45
|
+
* not the full MCP tool-definition shape.
|
|
46
|
+
*/
|
|
47
|
+
interface SourceToolMeta {
|
|
48
|
+
readonly name: string;
|
|
49
|
+
readonly visibility: ReadonlyArray<'model' | 'app'>;
|
|
50
|
+
}
|
|
51
|
+
export interface ToolsListCacheEntry {
|
|
52
|
+
readonly byName: ReadonlyMap<string, SourceToolMeta>;
|
|
53
|
+
readonly fetchedAt: number;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Tools-list cache keyed by connectorId. Short TTL — stale visibility
|
|
57
|
+
* decisions have small but real security impact, so cache invalidation
|
|
58
|
+
* is time-bounded (default 60s) with on-demand refresh.
|
|
59
|
+
*/
|
|
60
|
+
export declare class ToolsListCache {
|
|
61
|
+
private readonly ttlMs;
|
|
62
|
+
private readonly map;
|
|
63
|
+
constructor(ttlMs?: number);
|
|
64
|
+
get(connectorId: string): ToolsListCacheEntry | null;
|
|
65
|
+
set(connectorId: string, tools: Iterable<SourceToolMeta>): ToolsListCacheEntry;
|
|
66
|
+
invalidate(connectorId: string): void;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Register the MCP Apps inbound proxy routes on an Express app.
|
|
70
|
+
*
|
|
71
|
+
* GET /mcp-apps/resource?session=<id>&item=<id>
|
|
72
|
+
* Fetches the referenced McpAppsStackItem's `source.resourceUri`
|
|
73
|
+
* via resources/read on the source MCP server. Returns the HTML
|
|
74
|
+
* with the MIME declared by the source (typically
|
|
75
|
+
* `text/html;profile=mcp-app`) and spec-canonical CSP headers.
|
|
76
|
+
*
|
|
77
|
+
* POST /mcp-apps/tools-call
|
|
78
|
+
* Body: { session, item, tool, arguments? }.
|
|
79
|
+
* Resolves the stack item → connector → cached tools/list, gates
|
|
80
|
+
* on `_meta.ui.visibility` (must include 'app'), and proxies the
|
|
81
|
+
* call via MCP client. Cross-connector / unknown tool / model-
|
|
82
|
+
* only tool are all rejected.
|
|
83
|
+
*/
|
|
84
|
+
export declare function installMcpAppsInbound(app: Express, opts: McpAppsInboundOptions): ToolsListCache;
|
|
85
|
+
export {};
|
|
86
|
+
//# sourceMappingURL=mcp-apps-inbound.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-apps-inbound.d.ts","sourceRoot":"","sources":["../src/mcp-apps-inbound.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,SAAS,CAAC;AAG1D,OAAO,EACL,KAAK,iBAAiB,EAEtB,KAAK,YAAY,EAClB,MAAM,0BAA0B,CAAC;AAGlC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,0DAA0D;AAC1D,eAAO,MAAM,6BAA6B,cAAc,CAAC;AAEzD,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,UAAU,EAAE,iBAAiB,CAAC;IACvC,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,mEAAmE;IACnE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;GAKG;AACH,UAAU,cAAc;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC,OAAO,GAAG,KAAK,CAAC,CAAC;CACrD;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IACrD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED;;;;GAIG;AACH,qBAAa,cAAc;IAEb,OAAO,CAAC,QAAQ,CAAC,KAAK;IADlC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA0C;gBACjC,KAAK,GAAE,MAAe;IAEnD,GAAG,CAAC,WAAW,EAAE,MAAM,GAAG,mBAAmB,GAAG,IAAI;IAUpD,GAAG,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,CAAC,cAAc,CAAC,GAAG,mBAAmB;IAQ9E,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;CAGtC;AAwDD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,qBAAqB,GAC1B,cAAc,CAsIhB"}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Apps inbound hosting — resource proxy + tools/call visibility gate.
|
|
3
|
+
*
|
|
4
|
+
* When the ggui server hosts a third-party MCP App iframe, the browser
|
|
5
|
+
* CANNOT resolve `ui://` URIs directly (browser URL scheme set is
|
|
6
|
+
* limited to http/https/etc). The server owns the only path to the
|
|
7
|
+
* resource bytes:
|
|
8
|
+
*
|
|
9
|
+
* browser iframe
|
|
10
|
+
* ↓ https://<ggui-server>/mcp-apps/resource?session=…&item=…
|
|
11
|
+
* ggui-server proxy route
|
|
12
|
+
* ↓ MCP resources/read (via @modelcontextprotocol/sdk/client)
|
|
13
|
+
* source MCP server
|
|
14
|
+
* ↓ ui:// resource HTML
|
|
15
|
+
* ggui-server returns the HTML bytes to the iframe.
|
|
16
|
+
*
|
|
17
|
+
* Similarly, iframe-originated `tools/call` flows:
|
|
18
|
+
*
|
|
19
|
+
* iframe ----postMessage---> ggui client (in parent)
|
|
20
|
+
* ggui client -----fetch----> ggui-server /mcp-apps/tools-call
|
|
21
|
+
* ggui-server enforces visibility, proxies via MCP client to source.
|
|
22
|
+
*
|
|
23
|
+
* Visibility enforcement lives at the proxy boundary (this file), NOT
|
|
24
|
+
* in the client. Source `tools/list` is fetched + cached per connector;
|
|
25
|
+
* `_meta.ui.visibility` gates which tools iframe-originated calls can
|
|
26
|
+
* invoke. Cross-connector calls are rejected — the iframe for connector
|
|
27
|
+
* A can NEVER call tools on connector B.
|
|
28
|
+
*/
|
|
29
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
30
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
31
|
+
import { isMcpAppsStackItem } from '@ggui-ai/protocol/integrations/mcp-apps';
|
|
32
|
+
/** Default mount path for the MCP Apps inbound routes. */
|
|
33
|
+
export const DEFAULT_MCP_APPS_INBOUND_PATH = '/mcp-apps';
|
|
34
|
+
/**
|
|
35
|
+
* Tools-list cache keyed by connectorId. Short TTL — stale visibility
|
|
36
|
+
* decisions have small but real security impact, so cache invalidation
|
|
37
|
+
* is time-bounded (default 60s) with on-demand refresh.
|
|
38
|
+
*/
|
|
39
|
+
export class ToolsListCache {
|
|
40
|
+
ttlMs;
|
|
41
|
+
map = new Map();
|
|
42
|
+
constructor(ttlMs = 60_000) {
|
|
43
|
+
this.ttlMs = ttlMs;
|
|
44
|
+
}
|
|
45
|
+
get(connectorId) {
|
|
46
|
+
const entry = this.map.get(connectorId);
|
|
47
|
+
if (!entry)
|
|
48
|
+
return null;
|
|
49
|
+
if (Date.now() - entry.fetchedAt > this.ttlMs) {
|
|
50
|
+
this.map.delete(connectorId);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return entry;
|
|
54
|
+
}
|
|
55
|
+
set(connectorId, tools) {
|
|
56
|
+
const byName = new Map();
|
|
57
|
+
for (const t of tools)
|
|
58
|
+
byName.set(t.name, t);
|
|
59
|
+
const entry = { byName, fetchedAt: Date.now() };
|
|
60
|
+
this.map.set(connectorId, entry);
|
|
61
|
+
return entry;
|
|
62
|
+
}
|
|
63
|
+
invalidate(connectorId) {
|
|
64
|
+
this.map.delete(connectorId);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function connectSourceClient(connector) {
|
|
68
|
+
const transport = new StreamableHTTPClientTransport(new URL(connector.serverUrl), connector.auth?.bearer
|
|
69
|
+
? { requestInit: { headers: { Authorization: `Bearer ${connector.auth.bearer}` } } }
|
|
70
|
+
: {});
|
|
71
|
+
const client = new Client({ name: 'ggui-mcp-apps-proxy', version: '0.0.1' }, { capabilities: {} });
|
|
72
|
+
await client.connect(transport);
|
|
73
|
+
return client;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Parse the `_meta.ui.visibility` off an MCP tool declaration.
|
|
77
|
+
* Missing / empty defaults to `['model']` per spec convention (tools
|
|
78
|
+
* without explicit visibility are model-callable).
|
|
79
|
+
*/
|
|
80
|
+
function readVisibility(toolDecl) {
|
|
81
|
+
if (toolDecl === null || typeof toolDecl !== 'object')
|
|
82
|
+
return ['model'];
|
|
83
|
+
const meta = toolDecl._meta;
|
|
84
|
+
if (meta === null || typeof meta !== 'object')
|
|
85
|
+
return ['model'];
|
|
86
|
+
const ui = meta.ui;
|
|
87
|
+
if (ui === null || typeof ui !== 'object')
|
|
88
|
+
return ['model'];
|
|
89
|
+
const v = ui.visibility;
|
|
90
|
+
if (!Array.isArray(v) || v.length === 0)
|
|
91
|
+
return ['model'];
|
|
92
|
+
const out = [];
|
|
93
|
+
for (const x of v) {
|
|
94
|
+
if (x === 'model' || x === 'app')
|
|
95
|
+
out.push(x);
|
|
96
|
+
}
|
|
97
|
+
return out.length > 0 ? out : ['model'];
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Resolve `{session, item}` from the ggui session store to a
|
|
101
|
+
* {@link McpAppsStackItem}. Returns null if session or item is missing
|
|
102
|
+
* or the target is not an mcpApps variant.
|
|
103
|
+
*/
|
|
104
|
+
async function resolveMcpAppsItem(store, sessionId, itemId) {
|
|
105
|
+
const session = await store.get(sessionId);
|
|
106
|
+
if (!session)
|
|
107
|
+
return null;
|
|
108
|
+
const entry = session.stack.find((e) => e.id === itemId);
|
|
109
|
+
if (!entry || !isMcpAppsStackItem(entry))
|
|
110
|
+
return null;
|
|
111
|
+
return entry;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Register the MCP Apps inbound proxy routes on an Express app.
|
|
115
|
+
*
|
|
116
|
+
* GET /mcp-apps/resource?session=<id>&item=<id>
|
|
117
|
+
* Fetches the referenced McpAppsStackItem's `source.resourceUri`
|
|
118
|
+
* via resources/read on the source MCP server. Returns the HTML
|
|
119
|
+
* with the MIME declared by the source (typically
|
|
120
|
+
* `text/html;profile=mcp-app`) and spec-canonical CSP headers.
|
|
121
|
+
*
|
|
122
|
+
* POST /mcp-apps/tools-call
|
|
123
|
+
* Body: { session, item, tool, arguments? }.
|
|
124
|
+
* Resolves the stack item → connector → cached tools/list, gates
|
|
125
|
+
* on `_meta.ui.visibility` (must include 'app'), and proxies the
|
|
126
|
+
* call via MCP client. Cross-connector / unknown tool / model-
|
|
127
|
+
* only tool are all rejected.
|
|
128
|
+
*/
|
|
129
|
+
export function installMcpAppsInbound(app, opts) {
|
|
130
|
+
const path = opts.path ?? DEFAULT_MCP_APPS_INBOUND_PATH;
|
|
131
|
+
const cache = new ToolsListCache();
|
|
132
|
+
app.get(`${path}/resource`, async (req, res) => {
|
|
133
|
+
const sessionId = typeof req.query.session === 'string' ? req.query.session : '';
|
|
134
|
+
const itemId = typeof req.query.item === 'string' ? req.query.item : '';
|
|
135
|
+
if (!sessionId || !itemId) {
|
|
136
|
+
res.status(400).type('text/plain').send('Missing ?session and/or ?item');
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const item = await resolveMcpAppsItem(opts.sessionStore, sessionId, itemId);
|
|
140
|
+
if (!item) {
|
|
141
|
+
res.status(404).type('text/plain').send('Stack item not found');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const connector = await opts.connectors.get(item.source.connectorId);
|
|
145
|
+
if (!connector) {
|
|
146
|
+
res.status(404).type('text/plain').send('Unknown connector');
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// Dev/cache optimization: inline resourceContent bypasses source fetch.
|
|
150
|
+
if (typeof item.resourceContent === 'string' && item.resourceContent.length > 0) {
|
|
151
|
+
res.status(200).type('text/html;profile=mcp-app').send(item.resourceContent);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
let client = null;
|
|
155
|
+
try {
|
|
156
|
+
client = await connectSourceClient(connector);
|
|
157
|
+
const result = await client.readResource({ uri: item.source.resourceUri });
|
|
158
|
+
const first = result.contents[0];
|
|
159
|
+
if (!first || !('text' in first) || typeof first.text !== 'string') {
|
|
160
|
+
res.status(502).type('text/plain').send('Source returned non-text resource');
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
// CSP composition — use the source-declared field names verbatim;
|
|
164
|
+
// a stricter ggui baseline can be layered in a later slice.
|
|
165
|
+
const csp = composeCsp(item);
|
|
166
|
+
if (csp)
|
|
167
|
+
res.setHeader('Content-Security-Policy', csp);
|
|
168
|
+
res.status(200).type(first.mimeType ?? 'text/html;profile=mcp-app').send(first.text);
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
opts.logger.error('mcp_apps_resource_proxy_failed', {
|
|
172
|
+
sessionId,
|
|
173
|
+
itemId,
|
|
174
|
+
connectorId: item.source.connectorId,
|
|
175
|
+
error: String(err),
|
|
176
|
+
});
|
|
177
|
+
res.status(502).type('text/plain').send('Failed to fetch source resource');
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
if (client)
|
|
181
|
+
await client.close().catch(() => undefined);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
app.post(`${path}/tools-call`, async (req, res) => {
|
|
185
|
+
const body = (req.body ?? {});
|
|
186
|
+
const sessionId = typeof body.session === 'string' ? body.session : '';
|
|
187
|
+
const itemId = typeof body.item === 'string' ? body.item : '';
|
|
188
|
+
const toolName = typeof body.tool === 'string' ? body.tool : '';
|
|
189
|
+
if (!sessionId || !itemId || !toolName) {
|
|
190
|
+
res.status(400).json({ error: 'missing_fields' });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const item = await resolveMcpAppsItem(opts.sessionStore, sessionId, itemId);
|
|
194
|
+
if (!item) {
|
|
195
|
+
res.status(404).json({ error: 'item_not_found' });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const connector = await opts.connectors.get(item.source.connectorId);
|
|
199
|
+
if (!connector) {
|
|
200
|
+
res.status(404).json({ error: 'unknown_connector' });
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
let client = null;
|
|
204
|
+
try {
|
|
205
|
+
client = await connectSourceClient(connector);
|
|
206
|
+
// Cache tools/list per-connector. Visibility decisions read from
|
|
207
|
+
// the cache; a cold cache fetches via tools/list on the source.
|
|
208
|
+
let entry = cache.get(item.source.connectorId);
|
|
209
|
+
if (!entry) {
|
|
210
|
+
const list = await client.listTools();
|
|
211
|
+
entry = cache.set(item.source.connectorId, list.tools.map((t) => ({
|
|
212
|
+
name: t.name,
|
|
213
|
+
visibility: readVisibility(t),
|
|
214
|
+
})));
|
|
215
|
+
}
|
|
216
|
+
const meta = entry.byName.get(toolName);
|
|
217
|
+
if (!meta) {
|
|
218
|
+
res.status(404).json({ error: 'tool_not_found' });
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (!meta.visibility.includes('app')) {
|
|
222
|
+
// Model-only tool — not callable from an iframe view per
|
|
223
|
+
// MCP Apps visibility semantics.
|
|
224
|
+
res.status(403).json({ error: 'visibility_denied' });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
// Cross-connector check: the iframe for item A.connector can
|
|
228
|
+
// only call tools on A.connector. Since we resolved connector
|
|
229
|
+
// from `item.source.connectorId`, any other connector would
|
|
230
|
+
// require a different proxy path; this handler only serves
|
|
231
|
+
// `item.source.connectorId`. No-op enforcement here; see the
|
|
232
|
+
// test that exercises a forged cross-connector `tool` input
|
|
233
|
+
// (the tool name won't exist in THIS connector's tools-list).
|
|
234
|
+
const result = await client.callTool({
|
|
235
|
+
name: toolName,
|
|
236
|
+
arguments: body.arguments ?? {},
|
|
237
|
+
});
|
|
238
|
+
res.status(200).json(result);
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
opts.logger.error('mcp_apps_tools_call_proxy_failed', {
|
|
242
|
+
sessionId,
|
|
243
|
+
itemId,
|
|
244
|
+
connectorId: item.source.connectorId,
|
|
245
|
+
toolName,
|
|
246
|
+
error: String(err),
|
|
247
|
+
});
|
|
248
|
+
res.status(502).json({ error: 'proxy_failed' });
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
if (client)
|
|
252
|
+
await client.close().catch(() => undefined);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
return cache;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Compose a Content-Security-Policy header from an MCP App's declared
|
|
259
|
+
* CSP metadata. Minimal — uses spec-canonical field names verbatim.
|
|
260
|
+
* Returns null when no CSP was declared (caller leaves the default
|
|
261
|
+
* header in place).
|
|
262
|
+
*/
|
|
263
|
+
function composeCsp(item) {
|
|
264
|
+
const csp = item.csp;
|
|
265
|
+
if (!csp)
|
|
266
|
+
return null;
|
|
267
|
+
const parts = [];
|
|
268
|
+
if (csp.connectDomains && csp.connectDomains.length > 0) {
|
|
269
|
+
parts.push(`connect-src 'self' ${csp.connectDomains.join(' ')}`);
|
|
270
|
+
}
|
|
271
|
+
if (csp.resourceDomains && csp.resourceDomains.length > 0) {
|
|
272
|
+
parts.push(`default-src 'self' ${csp.resourceDomains.join(' ')}`, `script-src 'self' 'unsafe-inline' ${csp.resourceDomains.join(' ')}`, `style-src 'self' 'unsafe-inline' ${csp.resourceDomains.join(' ')}`);
|
|
273
|
+
}
|
|
274
|
+
if (csp.frameDomains && csp.frameDomains.length > 0) {
|
|
275
|
+
parts.push(`frame-src 'self' ${csp.frameDomains.join(' ')}`);
|
|
276
|
+
}
|
|
277
|
+
return parts.length > 0 ? parts.join('; ') : null;
|
|
278
|
+
}
|