@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.
Files changed (141) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +48 -0
  3. package/dist/admin-blueprints-transport.d.ts +114 -0
  4. package/dist/admin-blueprints-transport.d.ts.map +1 -0
  5. package/dist/admin-blueprints-transport.js +118 -0
  6. package/dist/admin-oauth-providers-transport.d.ts +40 -0
  7. package/dist/admin-oauth-providers-transport.d.ts.map +1 -0
  8. package/dist/admin-oauth-providers-transport.js +263 -0
  9. package/dist/auth.d.ts +39 -0
  10. package/dist/auth.d.ts.map +1 -0
  11. package/dist/auth.js +75 -0
  12. package/dist/build-mcp.d.ts +128 -0
  13. package/dist/build-mcp.d.ts.map +1 -0
  14. package/dist/build-mcp.js +113 -0
  15. package/dist/code-store-fs.d.ts +19 -0
  16. package/dist/code-store-fs.d.ts.map +1 -0
  17. package/dist/code-store-fs.js +98 -0
  18. package/dist/console-auth.d.ts +139 -0
  19. package/dist/console-auth.d.ts.map +1 -0
  20. package/dist/console-auth.js +102 -0
  21. package/dist/console-cache.d.ts +78 -0
  22. package/dist/console-cache.d.ts.map +1 -0
  23. package/dist/console-cache.js +105 -0
  24. package/dist/console-headers.d.ts +124 -0
  25. package/dist/console-headers.d.ts.map +1 -0
  26. package/dist/console-headers.js +49 -0
  27. package/dist/console-llm-trace.d.ts +66 -0
  28. package/dist/console-llm-trace.d.ts.map +1 -0
  29. package/dist/console-llm-trace.js +105 -0
  30. package/dist/console-payloads.d.ts +67 -0
  31. package/dist/console-payloads.d.ts.map +1 -0
  32. package/dist/console-payloads.js +105 -0
  33. package/dist/console-theme-routes.d.ts +111 -0
  34. package/dist/console-theme-routes.d.ts.map +1 -0
  35. package/dist/console-theme-routes.js +202 -0
  36. package/dist/console-timeline.d.ts +45 -0
  37. package/dist/console-timeline.d.ts.map +1 -0
  38. package/dist/console-timeline.js +169 -0
  39. package/dist/console-validator.d.ts +67 -0
  40. package/dist/console-validator.d.ts.map +1 -0
  41. package/dist/console-validator.js +105 -0
  42. package/dist/console-welcome.d.ts +7 -0
  43. package/dist/console-welcome.d.ts.map +1 -0
  44. package/dist/console-welcome.js +221 -0
  45. package/dist/csrf-middleware.d.ts +55 -0
  46. package/dist/csrf-middleware.d.ts.map +1 -0
  47. package/dist/csrf-middleware.js +138 -0
  48. package/dist/email-login.d.ts +174 -0
  49. package/dist/email-login.d.ts.map +1 -0
  50. package/dist/email-login.js +254 -0
  51. package/dist/email-resend.d.ts +29 -0
  52. package/dist/email-resend.d.ts.map +1 -0
  53. package/dist/email-resend.js +71 -0
  54. package/dist/email-sender-from-env.d.ts +34 -0
  55. package/dist/email-sender-from-env.d.ts.map +1 -0
  56. package/dist/email-sender-from-env.js +112 -0
  57. package/dist/email-smtp.d.ts +42 -0
  58. package/dist/email-smtp.d.ts.map +1 -0
  59. package/dist/email-smtp.js +81 -0
  60. package/dist/index.d.ts +102 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/index.js +122 -0
  63. package/dist/instructions-presets.d.ts +112 -0
  64. package/dist/instructions-presets.d.ts.map +1 -0
  65. package/dist/instructions-presets.js +195 -0
  66. package/dist/llm-backed-negotiator.d.ts +178 -0
  67. package/dist/llm-backed-negotiator.d.ts.map +1 -0
  68. package/dist/llm-backed-negotiator.js +579 -0
  69. package/dist/logger.d.ts +23 -0
  70. package/dist/logger.d.ts.map +1 -0
  71. package/dist/logger.js +41 -0
  72. package/dist/mcp-apps-inbound.d.ts +86 -0
  73. package/dist/mcp-apps-inbound.d.ts.map +1 -0
  74. package/dist/mcp-apps-inbound.js +278 -0
  75. package/dist/mcp-apps-outbound.d.ts +448 -0
  76. package/dist/mcp-apps-outbound.d.ts.map +1 -0
  77. package/dist/mcp-apps-outbound.js +1163 -0
  78. package/dist/mcp-mounts.d.ts +239 -0
  79. package/dist/mcp-mounts.d.ts.map +1 -0
  80. package/dist/mcp-mounts.js +222 -0
  81. package/dist/oauth-login-types.d.ts +160 -0
  82. package/dist/oauth-login-types.d.ts.map +1 -0
  83. package/dist/oauth-login-types.js +9 -0
  84. package/dist/oauth-login.d.ts +77 -0
  85. package/dist/oauth-login.d.ts.map +1 -0
  86. package/dist/oauth-login.js +455 -0
  87. package/dist/oauth-providers/github.d.ts +17 -0
  88. package/dist/oauth-providers/github.d.ts.map +1 -0
  89. package/dist/oauth-providers/github.js +89 -0
  90. package/dist/oauth-providers/google.d.ts +18 -0
  91. package/dist/oauth-providers/google.d.ts.map +1 -0
  92. package/dist/oauth-providers/google.js +59 -0
  93. package/dist/oauth-providers-store.d.ts +32 -0
  94. package/dist/oauth-providers-store.d.ts.map +1 -0
  95. package/dist/oauth-providers-store.js +291 -0
  96. package/dist/oauth.d.ts +347 -0
  97. package/dist/oauth.d.ts.map +1 -0
  98. package/dist/oauth.js +686 -0
  99. package/dist/pairing-transport.d.ts +99 -0
  100. package/dist/pairing-transport.d.ts.map +1 -0
  101. package/dist/pairing-transport.js +223 -0
  102. package/dist/rate-limit-middleware.d.ts +36 -0
  103. package/dist/rate-limit-middleware.d.ts.map +1 -0
  104. package/dist/rate-limit-middleware.js +57 -0
  105. package/dist/render-gate.d.ts +87 -0
  106. package/dist/render-gate.d.ts.map +1 -0
  107. package/dist/render-gate.js +77 -0
  108. package/dist/render-rate-limit.d.ts +59 -0
  109. package/dist/render-rate-limit.d.ts.map +1 -0
  110. package/dist/render-rate-limit.js +73 -0
  111. package/dist/render-signing.d.ts +98 -0
  112. package/dist/render-signing.d.ts.map +1 -0
  113. package/dist/render-signing.js +113 -0
  114. package/dist/request-context.d.ts +113 -0
  115. package/dist/request-context.d.ts.map +1 -0
  116. package/dist/request-context.js +154 -0
  117. package/dist/reserved-validators.d.ts +22 -0
  118. package/dist/reserved-validators.d.ts.map +1 -0
  119. package/dist/reserved-validators.js +101 -0
  120. package/dist/schema-compat.d.ts +167 -0
  121. package/dist/schema-compat.d.ts.map +1 -0
  122. package/dist/schema-compat.js +187 -0
  123. package/dist/security-headers-middleware.d.ts +38 -0
  124. package/dist/security-headers-middleware.d.ts.map +1 -0
  125. package/dist/security-headers-middleware.js +30 -0
  126. package/dist/server.d.ts +2060 -0
  127. package/dist/server.d.ts.map +1 -0
  128. package/dist/server.js +6338 -0
  129. package/dist/session-channel.d.ts +651 -0
  130. package/dist/session-channel.d.ts.map +1 -0
  131. package/dist/session-channel.js +1756 -0
  132. package/dist/storage.d.ts +89 -0
  133. package/dist/storage.d.ts.map +1 -0
  134. package/dist/storage.js +171 -0
  135. package/dist/thread-transport.d.ts +118 -0
  136. package/dist/thread-transport.d.ts.map +1 -0
  137. package/dist/thread-transport.js +478 -0
  138. package/dist/user-session-auth.d.ts +167 -0
  139. package/dist/user-session-auth.d.ts.map +1 -0
  140. package/dist/user-session-auth.js +148 -0
  141. 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
+ }