@calibress/curl-mcp-bridge 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # curl-mcp-bridge
2
+
3
+ A local MCP `stdio` bridge that proxies MCP JSON-RPC to a Calibress hosted `/mcp` HTTP endpoint.
4
+
5
+ This lets any MCP client that supports `stdio` (Claude Desktop, Roo, etc.) talk to the hosted multi-tenant MCP gateway.
6
+
7
+ ## Requirements
8
+
9
+ - Node.js >= 18
10
+
11
+ ## Usage
12
+
13
+ Set these env vars:
14
+
15
+ - `CALIBRESS_ENDPOINT` (required) — Hosted MCP URL (e.g. `https://enterprise-plan.dev.calibress.com/mcp`)
16
+ - `CALIBRESS_API_KEY` (required) — Tenant/project API key (Bearer). Prefer project-scoped keys so “project selection” is implicit.
17
+
18
+ Then run:
19
+
20
+ ```bash
21
+ CALIBRESS_ENDPOINT="https://enterprise-plan.dev.calibress.com/mcp" \
22
+ CALIBRESS_API_KEY="cmcp_..." \
23
+ node ./bin/curl-mcp-bridge.js
24
+ ```
25
+
26
+ CLI overrides are also available (`--endpoint`, `--api-key`, `--session-id`).
27
+
28
+ The bridge reads MCP messages from stdin and writes MCP responses to stdout (1 JSON object per line).
29
+
30
+ ## Run via npx (npm)
31
+
32
+ ```bash
33
+ CALIBRESS_ENDPOINT="https://enterprise-plan.dev.calibress.com/mcp" \
34
+ CALIBRESS_API_KEY="cmcp_..." \
35
+ npx -y @calibress/curl-mcp-bridge
36
+ ```
37
+
38
+ If you need to run an unpublished build, you can still run from GitHub:
39
+
40
+ ```bash
41
+ CALIBRESS_ENDPOINT="https://enterprise-plan.dev.calibress.com/mcp" \
42
+ CALIBRESS_API_KEY="cmcp_..." \
43
+ npx -y github:calibress/curl-mcp-bridge
44
+ ```
45
+
46
+ ## MCP client configuration
47
+
48
+ Example (Roo / Claude Desktop style):
49
+
50
+ ```json
51
+ {
52
+ "curl-mcp-enterprise-plan": {
53
+ "transport": "stdio",
54
+ "command": "node",
55
+ "args": ["/ABS/PATH/TO/curl-mcp-bridge/bin/curl-mcp-bridge.js"],
56
+ "env": {
57
+ "CALIBRESS_ENDPOINT": "https://enterprise-plan.dev.calibress.com/mcp",
58
+ "CALIBRESS_API_KEY": "cmcp_REPLACE_ME"
59
+ },
60
+ "alwaysAllow": ["curl_request", "mcp_version"]
61
+ }
62
+ }
63
+ ```
64
+
65
+ ## Session handling
66
+
67
+ - The hosted MCP endpoint issues `Mcp-Session-Id` on `initialize`.
68
+ - The bridge caches that session id in-process and automatically sends it on subsequent requests.
69
+ - If a client sends a request before `initialize`, the bridge will bootstrap a session and retry once.
70
+
71
+ ## Extensibility
72
+
73
+ The bridge forwards all JSON-RPC methods as-is.
74
+
75
+ When the hosted MCP gateway adds new tools (e.g. `list_connections`), existing clients automatically see them via `tools/list` without changing the bridge.
76
+
77
+ ## Notes
78
+
79
+ - To publish, bump `package.json` version and run `npm publish` (public access is configured).
@@ -0,0 +1,312 @@
1
+ #!/usr/bin/env node
2
+
3
+ import process from 'node:process'
4
+ import readline from 'node:readline'
5
+ import { createRequire } from 'node:module'
6
+
7
+ const require = createRequire(import.meta.url)
8
+ const { name: PACKAGE_NAME, version: PACKAGE_VERSION } = require('../package.json')
9
+
10
+ function parseArgs(argv) {
11
+ const args = {
12
+ endpoint: undefined,
13
+ apiKey: undefined,
14
+ sessionId: undefined,
15
+ verbose: false,
16
+ help: false,
17
+ version: false,
18
+ }
19
+
20
+ for (let i = 0; i < argv.length; i += 1) {
21
+ const current = argv[i]
22
+
23
+ if (current === '--help' || current === '-h') {
24
+ args.help = true
25
+ continue
26
+ }
27
+
28
+ if (current === '--version' || current === '-v') {
29
+ args.version = true
30
+ continue
31
+ }
32
+
33
+ if (current === '--verbose') {
34
+ args.verbose = true
35
+ continue
36
+ }
37
+
38
+ const [key, valueFromEquals] = current.split('=', 2)
39
+ const valueFromNext = argv[i + 1]
40
+
41
+ if (key === '--endpoint') {
42
+ args.endpoint = valueFromEquals ?? valueFromNext
43
+ if (valueFromEquals === undefined) i += 1
44
+ continue
45
+ }
46
+
47
+ if (key === '--api-key') {
48
+ args.apiKey = valueFromEquals ?? valueFromNext
49
+ if (valueFromEquals === undefined) i += 1
50
+ continue
51
+ }
52
+
53
+ if (key === '--session-id') {
54
+ args.sessionId = valueFromEquals ?? valueFromNext
55
+ if (valueFromEquals === undefined) i += 1
56
+ continue
57
+ }
58
+ }
59
+
60
+ return args
61
+ }
62
+
63
+ function printHelp() {
64
+ process.stderr.write(`${PACKAGE_NAME} ${PACKAGE_VERSION}\n\n`)
65
+ process.stderr.write(`Usage:\n`)
66
+ process.stderr.write(` CALIBRESS_ENDPOINT=... CALIBRESS_API_KEY=... curl-mcp-bridge\n\n`)
67
+ process.stderr.write(`Options:\n`)
68
+ process.stderr.write(` --endpoint <url> Override CALIBRESS_ENDPOINT\n`)
69
+ process.stderr.write(` --api-key <key> Override CALIBRESS_API_KEY\n`)
70
+ process.stderr.write(` --session-id <id> Force Mcp-Session-Id (debug)\n`)
71
+ process.stderr.write(` --verbose Log non-sensitive diagnostics to stderr\n`)
72
+ process.stderr.write(` --help Show help\n`)
73
+ process.stderr.write(` --version Show version\n`)
74
+ }
75
+
76
+ function jsonRpcError(id, code, message) {
77
+ return {
78
+ jsonrpc: '2.0',
79
+ id: id ?? null,
80
+ error: {
81
+ code,
82
+ message,
83
+ },
84
+ }
85
+ }
86
+
87
+ function normalizeContentItem(item) {
88
+ if (!item || typeof item !== 'object') return item
89
+ if (item.type !== 'json') return item
90
+
91
+ const value = Object.prototype.hasOwnProperty.call(item, 'data')
92
+ ? item.data
93
+ : Object.prototype.hasOwnProperty.call(item, 'json')
94
+ ? item.json
95
+ : item
96
+
97
+ const text = typeof value === 'string' ? value : JSON.stringify(value, null, 2)
98
+ return { type: 'text', text }
99
+ }
100
+
101
+ function normalizeMcpPayload(payload) {
102
+ const normalizeOne = (message) => {
103
+ if (!message || typeof message !== 'object') return message
104
+ if (!message.result || typeof message.result !== 'object') return message
105
+ if (!Array.isArray(message.result.content)) return message
106
+
107
+ return {
108
+ ...message,
109
+ result: {
110
+ ...message.result,
111
+ content: message.result.content.map(normalizeContentItem),
112
+ },
113
+ }
114
+ }
115
+
116
+ return Array.isArray(payload) ? payload.map(normalizeOne) : normalizeOne(payload)
117
+ }
118
+
119
+ function safeStringify(value) {
120
+ return JSON.stringify(value)
121
+ }
122
+
123
+ function logVerbose(enabled, message) {
124
+ if (!enabled) return
125
+ process.stderr.write(`[curl-mcp-bridge] ${message}\n`)
126
+ }
127
+
128
+ const args = parseArgs(process.argv.slice(2))
129
+
130
+ if (args.help) {
131
+ printHelp()
132
+ process.exit(0)
133
+ }
134
+
135
+ if (args.version) {
136
+ process.stdout.write(`${PACKAGE_VERSION}\n`)
137
+ process.exit(0)
138
+ }
139
+
140
+ const endpoint =
141
+ args.endpoint ??
142
+ process.env.CALIBRESS_ENDPOINT ??
143
+ process.env.CALIBRESS_MCP_ENDPOINT
144
+
145
+ const apiKey =
146
+ args.apiKey ??
147
+ process.env.CALIBRESS_API_KEY ??
148
+ process.env.CALIBRESS_MCP_API_KEY
149
+
150
+ if (!endpoint) {
151
+ process.stderr.write('Missing CALIBRESS_ENDPOINT\n')
152
+ process.exit(1)
153
+ }
154
+
155
+ if (!apiKey) {
156
+ process.stderr.write('Missing CALIBRESS_API_KEY\n')
157
+ process.exit(1)
158
+ }
159
+
160
+ let sessionId =
161
+ args.sessionId ??
162
+ process.env.CALIBRESS_MCP_SESSION_ID ??
163
+ undefined
164
+
165
+ async function postJson(body, extraHeaders) {
166
+ const headers = {
167
+ Authorization: `Bearer ${apiKey}`,
168
+ 'Content-Type': 'application/json',
169
+ Accept: 'application/json, text/event-stream',
170
+ ...extraHeaders,
171
+ }
172
+
173
+ const response = await fetch(endpoint, {
174
+ method: 'POST',
175
+ headers,
176
+ body: safeStringify(body),
177
+ })
178
+
179
+ const nextSessionId =
180
+ response.headers.get('mcp-session-id') ??
181
+ response.headers.get('Mcp-Session-Id')
182
+
183
+ if (nextSessionId) sessionId = nextSessionId
184
+
185
+ const text = await response.text()
186
+ if (!text) return null
187
+
188
+ try {
189
+ const parsed = JSON.parse(text)
190
+ return normalizeMcpPayload(parsed)
191
+ } catch {
192
+ const contentType = response.headers.get('content-type') ?? 'unknown'
193
+ const ngrokError = response.headers.get('ngrok-error-code')
194
+ const snippet = text.trim().slice(0, 160).replaceAll(/\s+/g, ' ')
195
+ const details = [
196
+ `HTTP ${response.status}`,
197
+ `content-type=${contentType}`,
198
+ ngrokError ? `ngrok-error-code=${ngrokError}` : null,
199
+ ]
200
+ .filter(Boolean)
201
+ .join(', ')
202
+
203
+ return jsonRpcError(
204
+ body?.id ?? null,
205
+ -32000,
206
+ `Upstream returned non-JSON response (${details}). Body: ${snippet}`
207
+ )
208
+ }
209
+ }
210
+
211
+ function shouldBootstrapFromError(payload) {
212
+ const message = payload?.error?.message
213
+ if (typeof message !== 'string') return false
214
+
215
+ if (message.includes('Mcp-Session-Id') && message.includes('required')) return true
216
+ if (message.toLowerCase().includes('server not initialized')) return true
217
+
218
+ return false
219
+ }
220
+
221
+ async function bootstrapSession() {
222
+ const initRequest = {
223
+ jsonrpc: '2.0',
224
+ id: 'bridge-init',
225
+ method: 'initialize',
226
+ params: {
227
+ protocolVersion: '2024-11-05',
228
+ capabilities: {},
229
+ clientInfo: {
230
+ name: PACKAGE_NAME,
231
+ version: PACKAGE_VERSION,
232
+ },
233
+ },
234
+ }
235
+
236
+ const payload = await postJson(initRequest, {})
237
+
238
+ if (!sessionId) {
239
+ const message = payload?.error?.message
240
+ throw new Error(
241
+ typeof message === 'string'
242
+ ? `Failed to establish session: ${message}`
243
+ : 'Failed to establish session'
244
+ )
245
+ }
246
+
247
+ return payload
248
+ }
249
+
250
+ async function proxyMessage(message) {
251
+ const headers = {}
252
+
253
+ if (sessionId) {
254
+ headers['Mcp-Session-Id'] = sessionId
255
+ }
256
+
257
+ const payload = await postJson(message, headers)
258
+
259
+ if (payload && shouldBootstrapFromError(payload)) {
260
+ logVerbose(args.verbose, 'Bootstrapping MCP session and retrying request')
261
+ await bootstrapSession()
262
+
263
+ const retryHeaders = {}
264
+ if (sessionId) retryHeaders['Mcp-Session-Id'] = sessionId
265
+
266
+ return postJson(message, retryHeaders)
267
+ }
268
+
269
+ return payload
270
+ }
271
+
272
+ function writeMessage(message) {
273
+ if (message === null || message === undefined) return
274
+ process.stdout.write(`${safeStringify(message)}\n`)
275
+ }
276
+
277
+ async function main() {
278
+ const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity })
279
+ for await (const line of rl) {
280
+ const trimmed = line.trim()
281
+ if (!trimmed) continue
282
+
283
+ let parsed
284
+ try {
285
+ parsed = JSON.parse(trimmed)
286
+ } catch {
287
+ writeMessage(jsonRpcError(null, -32700, 'Parse error'))
288
+ continue
289
+ }
290
+
291
+ try {
292
+ const response = await proxyMessage(parsed)
293
+ writeMessage(response)
294
+ } catch (error) {
295
+ const message =
296
+ error instanceof Error && typeof error.message === 'string'
297
+ ? error.message
298
+ : 'Upstream request failed'
299
+
300
+ writeMessage(jsonRpcError(parsed?.id ?? null, -32000, message))
301
+ }
302
+ }
303
+ }
304
+
305
+ main().catch((error) => {
306
+ const message =
307
+ error instanceof Error && typeof error.message === 'string'
308
+ ? error.message
309
+ : 'Bridge failed'
310
+ process.stderr.write(`[curl-mcp-bridge] ${message}\n`)
311
+ process.exit(1)
312
+ })
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@calibress/curl-mcp-bridge",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Local MCP stdio bridge that proxies to Calibress hosted /mcp over HTTP",
6
+ "keywords": [
7
+ "mcp",
8
+ "stdio",
9
+ "bridge",
10
+ "calibress"
11
+ ],
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/calibress/curl-mcp-bridge.git"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "bin": {
20
+ "curl-mcp-bridge": "bin/curl-mcp-bridge.js"
21
+ },
22
+ "scripts": {
23
+ "start": "node ./bin/curl-mcp-bridge.js"
24
+ },
25
+ "engines": {
26
+ "node": ">=18"
27
+ }
28
+ }