amalgm 0.0.0 → 0.0.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 (107) hide show
  1. package/README.md +37 -1
  2. package/bin/amalgm.js +8 -2
  3. package/lib/auth-store.js +223 -0
  4. package/lib/cli.js +1000 -0
  5. package/lib/paths.js +30 -0
  6. package/lib/supervisor.js +467 -0
  7. package/lib/tunnel-chat.js +328 -0
  8. package/lib/tunnel-events.js +499 -0
  9. package/package.json +29 -3
  10. package/runtime/README.md +4 -0
  11. package/runtime/lib/chatInput.js +306 -0
  12. package/runtime/lib/harnesses.js +988 -0
  13. package/runtime/lib/local/amalgmStore.js +128 -0
  14. package/runtime/lib/local/credentialResolver.js +425 -0
  15. package/runtime/lib/mcpApps/registry.js +619 -0
  16. package/runtime/package.json +5 -0
  17. package/runtime/scripts/amalgm-mcp/agents/rest.js +165 -0
  18. package/runtime/scripts/amalgm-mcp/agents/store.js +153 -0
  19. package/runtime/scripts/amalgm-mcp/agents/talk.js +1156 -0
  20. package/runtime/scripts/amalgm-mcp/agents/tools.js +210 -0
  21. package/runtime/scripts/amalgm-mcp/artifacts/advertise.js +132 -0
  22. package/runtime/scripts/amalgm-mcp/artifacts/rest.js +103 -0
  23. package/runtime/scripts/amalgm-mcp/artifacts/store.js +141 -0
  24. package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +402 -0
  25. package/runtime/scripts/amalgm-mcp/artifacts/tools.js +176 -0
  26. package/runtime/scripts/amalgm-mcp/browser/page.js +637 -0
  27. package/runtime/scripts/amalgm-mcp/browser/tools.js +688 -0
  28. package/runtime/scripts/amalgm-mcp/config.js +138 -0
  29. package/runtime/scripts/amalgm-mcp/credentials/rest.js +45 -0
  30. package/runtime/scripts/amalgm-mcp/deps.js +40 -0
  31. package/runtime/scripts/amalgm-mcp/email/inbound.js +215 -0
  32. package/runtime/scripts/amalgm-mcp/events/executor.js +179 -0
  33. package/runtime/scripts/amalgm-mcp/events/ingress.js +113 -0
  34. package/runtime/scripts/amalgm-mcp/events/matcher.js +125 -0
  35. package/runtime/scripts/amalgm-mcp/events/rest.js +200 -0
  36. package/runtime/scripts/amalgm-mcp/events/ring-buffer.js +19 -0
  37. package/runtime/scripts/amalgm-mcp/events/store.js +98 -0
  38. package/runtime/scripts/amalgm-mcp/events/tools.js +306 -0
  39. package/runtime/scripts/amalgm-mcp/events/webhook-url.js +28 -0
  40. package/runtime/scripts/amalgm-mcp/fs/rest.js +293 -0
  41. package/runtime/scripts/amalgm-mcp/index.js +100 -0
  42. package/runtime/scripts/amalgm-mcp/lib/chat-runner.js +167 -0
  43. package/runtime/scripts/amalgm-mcp/lib/email-md.js +288 -0
  44. package/runtime/scripts/amalgm-mcp/lib/mcp-resolver.js +63 -0
  45. package/runtime/scripts/amalgm-mcp/lib/prefs.js +393 -0
  46. package/runtime/scripts/amalgm-mcp/lib/storage.js +92 -0
  47. package/runtime/scripts/amalgm-mcp/lib/supabase.js +118 -0
  48. package/runtime/scripts/amalgm-mcp/lib/tool-result.js +177 -0
  49. package/runtime/scripts/amalgm-mcp/local/rest.js +80 -0
  50. package/runtime/scripts/amalgm-mcp/mcp-connections/rest.js +151 -0
  51. package/runtime/scripts/amalgm-mcp/notify/index.js +107 -0
  52. package/runtime/scripts/amalgm-mcp/server/http.js +335 -0
  53. package/runtime/scripts/amalgm-mcp/server/mcp.js +116 -0
  54. package/runtime/scripts/amalgm-mcp/slack/inbound.js +200 -0
  55. package/runtime/scripts/amalgm-mcp/tasks/executor.js +225 -0
  56. package/runtime/scripts/amalgm-mcp/tasks/rest.js +110 -0
  57. package/runtime/scripts/amalgm-mcp/tasks/schedule-normalization.js +85 -0
  58. package/runtime/scripts/amalgm-mcp/tasks/scheduler.js +105 -0
  59. package/runtime/scripts/amalgm-mcp/tasks/store.js +139 -0
  60. package/runtime/scripts/amalgm-mcp/tasks/tools.js +391 -0
  61. package/runtime/scripts/amalgm-mcp/user-api-keys/rest.js +105 -0
  62. package/runtime/scripts/amalgm-mcp/workspace/rest.js +389 -0
  63. package/runtime/scripts/chat-core/adapters/claude.js +163 -0
  64. package/runtime/scripts/chat-core/adapters/codex.js +313 -0
  65. package/runtime/scripts/chat-core/adapters/opencode.js +412 -0
  66. package/runtime/scripts/chat-core/auth.js +177 -0
  67. package/runtime/scripts/chat-core/contract.js +326 -0
  68. package/runtime/scripts/chat-core/credentials/store.js +212 -0
  69. package/runtime/scripts/chat-core/egress.js +87 -0
  70. package/runtime/scripts/chat-core/engine.js +195 -0
  71. package/runtime/scripts/chat-core/event-schema.js +231 -0
  72. package/runtime/scripts/chat-core/events.js +190 -0
  73. package/runtime/scripts/chat-core/index.js +11 -0
  74. package/runtime/scripts/chat-core/input.js +50 -0
  75. package/runtime/scripts/chat-core/normalizers/claude.js +450 -0
  76. package/runtime/scripts/chat-core/normalizers/codex.js +380 -0
  77. package/runtime/scripts/chat-core/normalizers/normalizer_spec.md +259 -0
  78. package/runtime/scripts/chat-core/normalizers/opencode.js +552 -0
  79. package/runtime/scripts/chat-core/normalizers/tool_contract.md +123 -0
  80. package/runtime/scripts/chat-core/normalizers/usage_contract.md +304 -0
  81. package/runtime/scripts/chat-core/parts.js +253 -0
  82. package/runtime/scripts/chat-core/recorder.js +65 -0
  83. package/runtime/scripts/chat-core/runtime.js +86 -0
  84. package/runtime/scripts/chat-core/server.js +163 -0
  85. package/runtime/scripts/chat-core/sse.js +196 -0
  86. package/runtime/scripts/chat-core/stores.js +100 -0
  87. package/runtime/scripts/chat-core/tool-display.js +149 -0
  88. package/runtime/scripts/chat-core/tool-shape.js +143 -0
  89. package/runtime/scripts/chat-core/tooling/mcp-bundle.js +161 -0
  90. package/runtime/scripts/chat-core/tooling/mcp-relay.js +97 -0
  91. package/runtime/scripts/chat-core/tooling/native-binaries.js +608 -0
  92. package/runtime/scripts/chat-core/tooling/system-prompt.js +20 -0
  93. package/runtime/scripts/chat-core/usage.js +343 -0
  94. package/runtime/scripts/chat-server/config.js +110 -0
  95. package/runtime/scripts/chat-server/db.js +529 -0
  96. package/runtime/scripts/chat-server/index.js +33 -0
  97. package/runtime/scripts/chat-server/model-catalog.js +327 -0
  98. package/runtime/scripts/chat-server.js +75 -0
  99. package/runtime/scripts/credential-adapter.js +129 -0
  100. package/runtime/scripts/fs-watcher.js +888 -0
  101. package/runtime/scripts/local-gateway.js +852 -0
  102. package/runtime/scripts/platform-context.txt +246 -0
  103. package/runtime/scripts/port-monitor.js +175 -0
  104. package/runtime/scripts/proxy-token-store.js +162 -0
  105. package/runtime/scripts/runtime-auth.js +163 -0
  106. package/runtime/scripts/test-claude-code-models.js +87 -0
  107. package/runtime/tsconfig.json +15 -0
@@ -0,0 +1,143 @@
1
+ 'use strict';
2
+
3
+ const { compactText, stringifyResult } = require('./tool-display');
4
+
5
+ function isRecord(value) {
6
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
7
+ }
8
+
9
+ function hostLabel(url) {
10
+ if (url === undefined || url === null || url === '') return '';
11
+ try {
12
+ const host = new URL(String(url)).hostname.replace(/^www\./, '');
13
+ return host || String(url);
14
+ } catch {
15
+ return compactText(url, 80);
16
+ }
17
+ }
18
+
19
+ function humanizeToolName(value = '') {
20
+ const text = String(value || '')
21
+ .replace(/^mcp__/, '')
22
+ .replace(/^amalgm[_-]/i, '')
23
+ .replace(/__/g, ' ')
24
+ .replace(/[_-]+/g, ' ')
25
+ .trim();
26
+ if (!text) return 'Tool';
27
+ return text.charAt(0).toUpperCase() + text.slice(1);
28
+ }
29
+
30
+ function parseMcpName(name = '') {
31
+ const text = String(name || '');
32
+ const mcp = text.match(/^mcp__([^_]+(?:_[^_]+)*)__([\s\S]+)$/);
33
+ if (mcp) return { server: mcp[1], tool: mcp[2] };
34
+ const amalgm = text.match(/^amalgm_(.+)$/);
35
+ if (amalgm) return { server: 'amalgm', tool: amalgm[1] };
36
+ return null;
37
+ }
38
+
39
+ function normalizeError(error, fallback = 'Tool failed') {
40
+ if (!error) return undefined;
41
+ if (typeof error === 'string') return { message: error };
42
+ if (isRecord(error)) {
43
+ const message = error.message || error.error || error.details || fallback;
44
+ return {
45
+ message: typeof message === 'string' ? message : stringifyResult(message),
46
+ ...(error.code ? { code: String(error.code) } : {}),
47
+ details: error,
48
+ };
49
+ }
50
+ return { message: String(error) || fallback };
51
+ }
52
+
53
+ function toolKind(name = '', input = {}, metadata = {}) {
54
+ const clean = String(name || '').toLowerCase();
55
+ const mcp = metadata.mcp || parseMcpName(name);
56
+ if (metadata.subagent || clean === 'task' || clean.includes('spawnagent') || clean.includes('collabagent')) return 'subagent';
57
+ if (clean.includes('todo')) return 'todo';
58
+ if (clean.includes('question') || clean.includes('askuser')) return 'question';
59
+ if (clean.includes('toolsearch')) return 'search';
60
+ if (clean.includes('bash') || clean.includes('command')) return 'bash';
61
+ if (clean.includes('websearch') || clean === 'websearch') return 'web_search';
62
+ if (clean.includes('browser') || mcp?.tool?.startsWith?.('browser_')) return 'browser';
63
+ if (clean.includes('webfetch') || clean === 'webfetch') return 'browser';
64
+ if (clean.includes('read')) return 'file_read';
65
+ if (clean.includes('write')) return 'file_write';
66
+ if (clean.includes('edit') || clean.includes('filechange')) return 'file_edit';
67
+ if (mcp) return 'mcp';
68
+ if (input && typeof input === 'object') {
69
+ if (input.command !== undefined) return 'bash';
70
+ if (input.url !== undefined) return 'browser';
71
+ if (input.query !== undefined) return 'web_search';
72
+ if (input.file_path !== undefined || input.filePath !== undefined || input.path !== undefined) return 'file_read';
73
+ }
74
+ return 'unknown';
75
+ }
76
+
77
+ function titleFromMcp(tool, input = {}) {
78
+ const name = String(tool || '');
79
+ const action = name.replace(/^browser_/, '');
80
+ if (name === 'browser_navigate') return input.url ? `Opening ${hostLabel(input.url)}` : 'Opening browser page';
81
+ if (name === 'browser_tabs_new') return input.url ? `Opening browser tab ${hostLabel(input.url)}` : 'Opening browser tab';
82
+ if (name === 'browser_snapshot') return 'Reading browser page';
83
+ if (name === 'browser_screenshot') return 'Taking browser screenshot';
84
+ if (name === 'browser_type') return input.text ? `Typing "${compactText(input.text, 60)}"` : 'Typing in browser';
85
+ if (name === 'browser_click') return 'Clicking in browser';
86
+ if (name === 'browser_wait_for_load_state') return 'Waiting for page load';
87
+ if (name === 'artifacts_register') return input.name ? `Registering artifact ${compactText(input.name, 60)}` : 'Registering artifact';
88
+ if (name === 'artifacts_start') return 'Starting artifact';
89
+ if (name === 'artifacts_stop') return 'Stopping artifact';
90
+ if (name === 'artifacts_list') return 'Listing artifacts';
91
+ if (name === 'agents_list') return 'Listing agents';
92
+ if (name === 'talk_to_agent') return 'Talking to agent';
93
+ return humanizeToolName(action || name);
94
+ }
95
+
96
+ function titleForTool({ name, input = {}, output, error, metadata = {}, fallback } = {}) {
97
+ if (metadata.actionDescriptor) return compactText(metadata.actionDescriptor, 160);
98
+ if (metadata.title) return compactText(metadata.title, 160);
99
+
100
+ const mcp = metadata.mcp || parseMcpName(name);
101
+ if (mcp) return titleFromMcp(mcp.tool, input);
102
+
103
+ const clean = String(name || '').toLowerCase();
104
+ if (metadata.action?.type === 'search') {
105
+ return `Searching ${compactText(metadata.action.query || input.query || 'the web', 80)}`;
106
+ }
107
+ if (metadata.action?.type === 'openPage') {
108
+ const host = hostLabel(metadata.action.url || input.url || input.query);
109
+ return host ? `Opening ${host}` : 'Opening web page';
110
+ }
111
+ if (metadata.action?.type === 'findInPage') {
112
+ const pattern = metadata.action.pattern || input.pattern;
113
+ const url = metadata.action.url || input.url;
114
+ const host = hostLabel(url) || 'current page';
115
+ return pattern ? `Finding "${compactText(pattern, 60)}" on ${host}` : `Finding text on ${host}`;
116
+ }
117
+ if (clean.includes('toolsearch')) {
118
+ const selected = String(input.query || '').replace(/^select:/i, '').trim();
119
+ return selected ? `Selecting ${compactText(selected, 80)}` : 'Selecting tool';
120
+ }
121
+ if (clean.includes('websearch')) return input.query ? `Searching ${compactText(input.query, 80)}` : 'Searching the web';
122
+ if (clean.includes('webfetch')) return input.url ? `Opening ${hostLabel(input.url)}` : 'Opening web page';
123
+ if (clean === 'bash' || clean.includes('command')) return input.command ? `$ ${compactText(input.command, 120)}` : 'Running command';
124
+ if (clean === 'task' || metadata.subagent) return compactText(input.description || metadata.description || fallback || 'Running subagent', 120);
125
+ if (error) return normalizeError(error)?.message || 'Tool failed';
126
+ return compactText(fallback || humanizeToolName(name), 120);
127
+ }
128
+
129
+ function mcpMetadataFromName(name) {
130
+ const mcp = parseMcpName(name);
131
+ return mcp ? { mcp } : {};
132
+ }
133
+
134
+ module.exports = {
135
+ hostLabel,
136
+ humanizeToolName,
137
+ isRecord,
138
+ mcpMetadataFromName,
139
+ normalizeError,
140
+ parseMcpName,
141
+ titleForTool,
142
+ toolKind,
143
+ };
@@ -0,0 +1,161 @@
1
+ 'use strict';
2
+
3
+ const { getRuntimeToken } = require('../../runtime-auth');
4
+
5
+ function headerArrayToRecord(headers) {
6
+ const out = {};
7
+ for (const header of Array.isArray(headers) ? headers : []) {
8
+ const name = String(header?.name || '').trim();
9
+ const value = String(header?.value || '');
10
+ if (name) out[name] = value;
11
+ }
12
+ return out;
13
+ }
14
+
15
+ function recordToHeaderArray(headers) {
16
+ if (!headers || typeof headers !== 'object') return [];
17
+ return Object.entries(headers)
18
+ .filter(([name, value]) => name && value != null)
19
+ .map(([name, value]) => ({ name, value: String(value) }));
20
+ }
21
+
22
+ function safeName(name) {
23
+ return String(name || '')
24
+ .trim()
25
+ .replace(/[^A-Za-z0-9_.-]+/g, '_')
26
+ .replace(/^_+|_+$/g, '')
27
+ || 'mcp';
28
+ }
29
+
30
+ function normalizeType(type) {
31
+ const clean = String(type || '').toLowerCase();
32
+ if (clean === 'sse') return 'sse';
33
+ return 'http';
34
+ }
35
+
36
+ function normalizeServer(server) {
37
+ if (!server || typeof server !== 'object') return null;
38
+ const name = safeName(server.name);
39
+ const url = String(server.url || '').trim();
40
+ if (!name || !url) return null;
41
+ return {
42
+ name,
43
+ type: normalizeType(server.type),
44
+ url,
45
+ headers: Array.isArray(server.headers)
46
+ ? server.headers
47
+ : recordToHeaderArray(server.headers),
48
+ };
49
+ }
50
+
51
+ function localBaseUrl(contract) {
52
+ return String(contract.localBaseUrl || `http://127.0.0.1:${process.env.CHAT_SERVER_PORT || 8084}`).replace(/\/$/, '');
53
+ }
54
+
55
+ function amalgmMcpServer(contract) {
56
+ const port = process.env.AMALGM_MCP_PORT || '8083';
57
+ const url = process.env.AMALGM_MCP_URL || `http://127.0.0.1:${port}/mcp`;
58
+ const headers = [
59
+ { name: 'x-amalgm-runtime-token', value: getRuntimeToken() },
60
+ { name: 'x-amalgm-session-id', value: contract.sessionId },
61
+ { name: 'x-amalgm-user-id', value: contract.userId || '' },
62
+ { name: 'x-amalgm-computer-id', value: contract.computerId || '' },
63
+ ].filter((h) => h.value);
64
+ return { name: 'amalgm', type: 'http', url, headers };
65
+ }
66
+
67
+ function mcpServers(contract) {
68
+ const byName = new Map();
69
+ const add = (server) => {
70
+ const normalized = normalizeServer(server);
71
+ if (normalized) byName.set(normalized.name, normalized);
72
+ };
73
+ add(amalgmMcpServer(contract));
74
+ for (const server of Array.isArray(contract.mcpServers) ? contract.mcpServers : []) add(server);
75
+ return [...byName.values()];
76
+ }
77
+
78
+ function relayUrl(contract, serverName) {
79
+ return `${localBaseUrl(contract)}/mcp-relay/${encodeURIComponent(contract.sessionId)}/${encodeURIComponent(serverName)}`;
80
+ }
81
+
82
+ function relayedMcpServers(contract) {
83
+ const runtimeToken = getRuntimeToken();
84
+ return mcpServers(contract).map((server) => ({
85
+ ...server,
86
+ url: relayUrl(contract, server.name),
87
+ headers: runtimeToken
88
+ ? [{ name: 'x-amalgm-runtime-token', value: runtimeToken }]
89
+ : [],
90
+ }));
91
+ }
92
+
93
+ function findMcpRelayTarget(contract, serverName) {
94
+ const clean = safeName(serverName);
95
+ return mcpServers(contract).find((server) => server.name === clean) || null;
96
+ }
97
+
98
+ function toClaudeMcpServers(contract) {
99
+ const out = {};
100
+ for (const server of relayedMcpServers(contract)) {
101
+ out[server.name] = {
102
+ type: server.type === 'sse' ? 'sse' : 'http',
103
+ url: server.url,
104
+ headers: headerArrayToRecord(server.headers),
105
+ alwaysLoad: server.name === 'amalgm' ? true : undefined,
106
+ };
107
+ }
108
+ return out;
109
+ }
110
+
111
+ function toOpenCodeMcpConfig(contract) {
112
+ const out = {};
113
+ for (const server of relayedMcpServers(contract)) {
114
+ out[server.name] = {
115
+ type: 'remote',
116
+ url: server.url,
117
+ enabled: true,
118
+ headers: headerArrayToRecord(server.headers),
119
+ oauth: false,
120
+ timeout: 15000,
121
+ };
122
+ }
123
+ return out;
124
+ }
125
+
126
+ function tomlString(value) {
127
+ return JSON.stringify(String(value || ''));
128
+ }
129
+
130
+ function toCodexMcpToml(contract) {
131
+ const lines = [];
132
+ const runtimeToken = getRuntimeToken();
133
+ for (const server of relayedMcpServers(contract)) {
134
+ lines.push(`[mcp_servers.${server.name}]`);
135
+ lines.push(`url = ${tomlString(server.url)}`);
136
+ if (runtimeToken) lines.push('bearer_token_env_var = "AMALGM_RUNTIME_TOKEN"');
137
+ lines.push('');
138
+ }
139
+ return lines.join('\n');
140
+ }
141
+
142
+ function mcpSummary(contract) {
143
+ return mcpServers(contract).map((server) => ({
144
+ name: server.name,
145
+ type: server.type,
146
+ url: server.url,
147
+ hasHeaders: server.headers.length > 0,
148
+ }));
149
+ }
150
+
151
+ module.exports = {
152
+ findMcpRelayTarget,
153
+ headerArrayToRecord,
154
+ mcpServers,
155
+ mcpSummary,
156
+ relayedMcpServers,
157
+ safeName,
158
+ toClaudeMcpServers,
159
+ toCodexMcpToml,
160
+ toOpenCodeMcpConfig,
161
+ };
@@ -0,0 +1,97 @@
1
+ 'use strict';
2
+
3
+ const { findMcpRelayTarget, headerArrayToRecord } = require('./mcp-bundle');
4
+
5
+ const HOP_HEADERS = new Set([
6
+ 'connection',
7
+ 'content-length',
8
+ 'host',
9
+ 'keep-alive',
10
+ 'proxy-authenticate',
11
+ 'proxy-authorization',
12
+ 'te',
13
+ 'trailer',
14
+ 'transfer-encoding',
15
+ 'upgrade',
16
+ ]);
17
+
18
+ function parseMcpRelayPath(pathname) {
19
+ const parts = String(pathname || '').split('/').filter(Boolean);
20
+ if (parts[0] !== 'mcp-relay' || !parts[1] || !parts[2]) return null;
21
+ return {
22
+ sessionId: decodeURIComponent(parts[1]),
23
+ serverName: decodeURIComponent(parts[2]),
24
+ };
25
+ }
26
+
27
+ function copyRequestHeaders(req, extraHeaders) {
28
+ const headers = {};
29
+ for (const [name, value] of Object.entries(req.headers || {})) {
30
+ const lower = name.toLowerCase();
31
+ if (HOP_HEADERS.has(lower)) continue;
32
+ if (lower === 'authorization') continue;
33
+ headers[name] = Array.isArray(value) ? value.join(', ') : String(value);
34
+ }
35
+ Object.assign(headers, extraHeaders);
36
+ return headers;
37
+ }
38
+
39
+ async function readBody(req) {
40
+ const chunks = [];
41
+ for await (const chunk of req) chunks.push(Buffer.from(chunk));
42
+ return chunks.length ? Buffer.concat(chunks) : null;
43
+ }
44
+
45
+ async function forwardMcpRelay({ req, res, contract, serverName, search = '' }) {
46
+ const target = findMcpRelayTarget(contract, serverName);
47
+ if (!target) {
48
+ res.writeHead(404, { 'Content-Type': 'application/json' });
49
+ res.end(JSON.stringify({ error: 'Unknown MCP server', serverName }));
50
+ return;
51
+ }
52
+
53
+ const upstream = `${target.url}${search || ''}`;
54
+ const extraHeaders = {
55
+ ...headerArrayToRecord(target.headers),
56
+ 'x-amalgm-session-id': contract.sessionId,
57
+ 'x-amalgm-user-id': contract.userId || '',
58
+ 'x-amalgm-computer-id': contract.computerId || '',
59
+ };
60
+ for (const key of Object.keys(extraHeaders)) {
61
+ if (!extraHeaders[key]) delete extraHeaders[key];
62
+ }
63
+
64
+ const body = ['GET', 'HEAD'].includes(req.method) ? undefined : await readBody(req);
65
+ const response = await fetch(upstream, {
66
+ method: req.method,
67
+ headers: copyRequestHeaders(req, extraHeaders),
68
+ body,
69
+ redirect: 'manual',
70
+ });
71
+
72
+ const headers = {};
73
+ response.headers.forEach((value, name) => {
74
+ if (!HOP_HEADERS.has(name.toLowerCase())) headers[name] = value;
75
+ });
76
+ res.writeHead(response.status, headers);
77
+ if (!response.body) {
78
+ res.end();
79
+ return;
80
+ }
81
+ const reader = response.body.getReader();
82
+ try {
83
+ while (true) {
84
+ const { done, value } = await reader.read();
85
+ if (done) break;
86
+ if (value) res.write(Buffer.from(value));
87
+ }
88
+ res.end();
89
+ } catch (err) {
90
+ try { res.destroy(err); } catch {}
91
+ }
92
+ }
93
+
94
+ module.exports = {
95
+ forwardMcpRelay,
96
+ parseMcpRelayPath,
97
+ };