@cargo-ai/cli 1.0.31 → 1.0.32
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/build/commands/mcp.d.ts +4 -0
- package/build/commands/mcp.d.ts.map +1 -0
- package/build/commands/mcp.js +192 -0
- package/build/index.js +6 -1
- package/build/proxy.d.ts +12 -0
- package/build/proxy.d.ts.map +1 -1
- package/build/proxy.js +39 -7
- package/package.json +6 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../../src/commands/mcp.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAerC,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,GAAG,IAAI,CAqD3E"}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { getConfig } from "../config.js";
|
|
5
|
+
import { getProxyFetch } from "../proxy.js";
|
|
6
|
+
import { ExitCodes, failWith, handleApiCall, info } from "./runHandler.js";
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const { version } = require("../../package.json");
|
|
9
|
+
// The prod REST API and the prod MCP server are the same backend image behind
|
|
10
|
+
// two hostnames; the MCP one (mcp.getcargo.io) is tuned for long-lived streaming.
|
|
11
|
+
// Point the bridge there when the CLI is talking to prod, but leave any other
|
|
12
|
+
// base URL (staging, localhost) untouched — that same host serves the MCP routes.
|
|
13
|
+
const PROD_API_HOST = "api.getcargo.io";
|
|
14
|
+
const PROD_MCP_HOST = "mcp.getcargo.io";
|
|
15
|
+
export function registerMcpCommand(parent, getApi) {
|
|
16
|
+
parent
|
|
17
|
+
.command("mcp")
|
|
18
|
+
.description("Run a local MCP server that bridges to a Cargo MCP server over stdio")
|
|
19
|
+
.option("--server <uuid>", "MCP server UUID to expose (default: CARGO_MCP_SERVER_UUID, or the workspace's only MCP server)")
|
|
20
|
+
.addHelpText("after", `
|
|
21
|
+
Exposes a Cargo-hosted MCP server as a local stdio MCP server, so any stdio MCP
|
|
22
|
+
client (Claude Code, Claude Desktop, Cursor, ...) can use it with the CLI's own
|
|
23
|
+
credentials — no token to copy into client config.
|
|
24
|
+
|
|
25
|
+
Register it with a client, e.g. Claude Code:
|
|
26
|
+
$ claude mcp add cargo -- cargo-ai mcp --server <uuid>
|
|
27
|
+
|
|
28
|
+
Environment:
|
|
29
|
+
CARGO_MCP_SERVER_UUID default --server value
|
|
30
|
+
CARGO_MCP_BASE_URL override the MCP endpoint origin (advanced/local dev)
|
|
31
|
+
|
|
32
|
+
Reads auth from the same place as every other command (CARGO_API_TOKEN or the
|
|
33
|
+
credentials file). stdout carries the MCP protocol; all logs go to stderr.`)
|
|
34
|
+
.action(async (opts) => {
|
|
35
|
+
const { accessToken, baseUrl, workspaceUuid } = getConfig();
|
|
36
|
+
if (accessToken === undefined) {
|
|
37
|
+
failWith('Not authenticated. Run "cargo-ai login --token <token>" or "cargo-ai login --oauth".', { code: ExitCodes.NotAuthenticated });
|
|
38
|
+
}
|
|
39
|
+
const serverUuid = await resolveServerUuid(opts.server, getApi);
|
|
40
|
+
const endpoint = resolveMcpEndpoint(baseUrl, serverUuid);
|
|
41
|
+
const headers = {
|
|
42
|
+
authorization: `Bearer ${accessToken}`,
|
|
43
|
+
"x-cargo-origin": "cli",
|
|
44
|
+
"x-cargo-origin-version": version,
|
|
45
|
+
};
|
|
46
|
+
if (workspaceUuid !== undefined) {
|
|
47
|
+
headers["selected-workspace-uuid"] = workspaceUuid;
|
|
48
|
+
}
|
|
49
|
+
info(`Cargo MCP bridge -> ${endpoint}`);
|
|
50
|
+
await runBridge(endpoint, headers);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
const resolveServerUuid = async (explicit, getApi) => {
|
|
54
|
+
if (explicit !== undefined)
|
|
55
|
+
return explicit;
|
|
56
|
+
const fromEnv = process.env["CARGO_MCP_SERVER_UUID"];
|
|
57
|
+
if (fromEnv !== undefined && fromEnv.length > 0)
|
|
58
|
+
return fromEnv;
|
|
59
|
+
const api = getApi();
|
|
60
|
+
const { mcpServers } = await handleApiCall(() => api.ai.mcpServer.all(), {
|
|
61
|
+
spinner: false,
|
|
62
|
+
});
|
|
63
|
+
if (mcpServers.length === 0) {
|
|
64
|
+
failWith("No MCP server in this workspace. Create one with 'cargo-ai ai mcp-server create --name ...', then pass --server <uuid>.", { code: ExitCodes.InvalidUsage });
|
|
65
|
+
}
|
|
66
|
+
const only = mcpServers[0];
|
|
67
|
+
if (mcpServers.length > 1 || only === undefined) {
|
|
68
|
+
failWith("Multiple MCP servers found; pass --server <uuid>.", {
|
|
69
|
+
code: ExitCodes.InvalidUsage,
|
|
70
|
+
extra: {
|
|
71
|
+
servers: mcpServers.map((server) => ({
|
|
72
|
+
uuid: server.uuid,
|
|
73
|
+
name: server.name,
|
|
74
|
+
})),
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return only.uuid;
|
|
79
|
+
};
|
|
80
|
+
const resolveMcpEndpoint = (baseUrl, uuid) => {
|
|
81
|
+
const override = process.env["CARGO_MCP_BASE_URL"];
|
|
82
|
+
const origin = override !== undefined && override.length > 0
|
|
83
|
+
? override
|
|
84
|
+
: mapApiHostToMcp(baseUrl);
|
|
85
|
+
return `${origin.replace(/\/+$/, "")}/v1/ai/mcpServers/${uuid}/mcp`;
|
|
86
|
+
};
|
|
87
|
+
const mapApiHostToMcp = (baseUrl) => {
|
|
88
|
+
try {
|
|
89
|
+
const url = new URL(baseUrl);
|
|
90
|
+
if (url.hostname === PROD_API_HOST) {
|
|
91
|
+
url.hostname = PROD_MCP_HOST;
|
|
92
|
+
return url.origin;
|
|
93
|
+
}
|
|
94
|
+
return baseUrl;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return baseUrl;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
// Pipe two MCP transports message-for-message: the local stdio server (facing the
|
|
101
|
+
// MCP client) and the remote streamable-HTTP client (facing Cargo). Forwarding raw
|
|
102
|
+
// JSON-RPC keeps the bridge transparent — tools, resources, and long-running-tool
|
|
103
|
+
// progress notifications all pass through without enumerating capabilities here.
|
|
104
|
+
const runBridge = async (endpoint, headers) => {
|
|
105
|
+
// Prefer a proxied fetch when HTTPS_PROXY/HTTP_PROXY applies — Node's
|
|
106
|
+
// built-in fetch ignores those env vars, so without this the bridge would
|
|
107
|
+
// fail in proxied environments even though server discovery (Axios) works.
|
|
108
|
+
const remote = new StreamableHTTPClientTransport(new URL(endpoint), {
|
|
109
|
+
requestInit: { headers },
|
|
110
|
+
fetch: getProxyFetch(endpoint),
|
|
111
|
+
});
|
|
112
|
+
const local = new StdioServerTransport();
|
|
113
|
+
await new Promise((resolve, reject) => {
|
|
114
|
+
const state = { closed: false };
|
|
115
|
+
// Shared handlers so finish/fail can remove whichever listeners are still
|
|
116
|
+
// registered. process.once / stdin.once only drop the event that fired; the
|
|
117
|
+
// others would otherwise keep the event loop alive and hang the process
|
|
118
|
+
// after resolve()/reject().
|
|
119
|
+
const onSignal = () => {
|
|
120
|
+
finish("Cargo MCP: shutting down.");
|
|
121
|
+
};
|
|
122
|
+
const onStdinGone = () => {
|
|
123
|
+
finish("Cargo MCP: client disconnected.");
|
|
124
|
+
};
|
|
125
|
+
const teardown = () => {
|
|
126
|
+
process.removeListener("SIGINT", onSignal);
|
|
127
|
+
process.removeListener("SIGTERM", onSignal);
|
|
128
|
+
process.stdin.removeListener("end", onStdinGone);
|
|
129
|
+
process.stdin.removeListener("close", onStdinGone);
|
|
130
|
+
void remote.close().catch(() => { });
|
|
131
|
+
void local.close().catch(() => { });
|
|
132
|
+
};
|
|
133
|
+
// Client disconnect and process signals are normal ends — resolve so the
|
|
134
|
+
// command exits 0. Upstream/startup/stdio failures must reject so callers
|
|
135
|
+
// and MCP clients see a non-zero exit (CLI contract: 0 means success).
|
|
136
|
+
const finish = (reason) => {
|
|
137
|
+
if (state.closed)
|
|
138
|
+
return;
|
|
139
|
+
state.closed = true;
|
|
140
|
+
if (reason !== undefined)
|
|
141
|
+
info(reason);
|
|
142
|
+
teardown();
|
|
143
|
+
resolve();
|
|
144
|
+
};
|
|
145
|
+
const fail = (message) => {
|
|
146
|
+
if (state.closed)
|
|
147
|
+
return;
|
|
148
|
+
state.closed = true;
|
|
149
|
+
teardown();
|
|
150
|
+
reject(new Error(message));
|
|
151
|
+
};
|
|
152
|
+
// Forward raw JSON-RPC both ways. A failing transport reports the reason via
|
|
153
|
+
// onerror (which fires before the send rejection), so the reason is logged
|
|
154
|
+
// there once; these catches only need to guarantee teardown.
|
|
155
|
+
remote.onmessage = (message) => {
|
|
156
|
+
void local.send(message).catch(() => finish());
|
|
157
|
+
};
|
|
158
|
+
local.onmessage = (message) => {
|
|
159
|
+
void remote.send(message).catch(() => finish());
|
|
160
|
+
};
|
|
161
|
+
// Reject with the failure reason once. Stay silent once we're already
|
|
162
|
+
// closing: the in-flight SSE stream aborting during our own shutdown surfaces
|
|
163
|
+
// here as expected noise.
|
|
164
|
+
remote.onerror = (error) => {
|
|
165
|
+
fail(`Cargo MCP: upstream error: ${error.message}`);
|
|
166
|
+
};
|
|
167
|
+
local.onerror = (error) => {
|
|
168
|
+
fail(`Cargo MCP: stdio error: ${error.message}`);
|
|
169
|
+
};
|
|
170
|
+
remote.onclose = () => fail("Cargo MCP: upstream closed.");
|
|
171
|
+
local.onclose = () => finish("Cargo MCP: client disconnected.");
|
|
172
|
+
process.once("SIGINT", onSignal);
|
|
173
|
+
process.once("SIGTERM", onSignal);
|
|
174
|
+
// @modelcontextprotocol/sdk 1.29.0 StdioServerTransport only listens for
|
|
175
|
+
// stdin `data`/`error` — it never wires `end`/`close`, so local.onclose
|
|
176
|
+
// above does not fire when the MCP client closes the pipe without a
|
|
177
|
+
// signal. Detect stdin EOF ourselves so we tear down the upstream
|
|
178
|
+
// streamable-HTTP session instead of leaving a zombie bridge.
|
|
179
|
+
// See https://github.com/modelcontextprotocol/typescript-sdk/issues/2002
|
|
180
|
+
if (process.stdin.readableEnded === true ||
|
|
181
|
+
process.stdin.destroyed === true) {
|
|
182
|
+
onStdinGone();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
process.stdin.once("end", onStdinGone);
|
|
186
|
+
process.stdin.once("close", onStdinGone);
|
|
187
|
+
Promise.all([remote.start(), local.start()]).catch((error) => {
|
|
188
|
+
fail(`Cargo MCP: failed to start bridge: ${describe(error)}`);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
};
|
|
192
|
+
const describe = (error) => error instanceof Error ? error.message : String(error);
|
package/build/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
-
import { registerCommands as registerCdkCommands } from "@cargo-ai/cdk/cli";
|
|
3
|
+
import { registerCommands as registerCdkCommands, registerManifestCommands, } from "@cargo-ai/cdk/cli";
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { createApi } from "./api.js";
|
|
6
6
|
import { registerAiCommands } from "./commands/ai/index.js";
|
|
@@ -12,6 +12,7 @@ import { registerContextCommands } from "./commands/context/index.js";
|
|
|
12
12
|
import { registerExpressionCommands } from "./commands/expression/index.js";
|
|
13
13
|
import { registerHostingCommands } from "./commands/hosting/index.js";
|
|
14
14
|
import { registerInitCommand } from "./commands/init.js";
|
|
15
|
+
import { registerMcpCommand } from "./commands/mcp.js";
|
|
15
16
|
import { registerOrchestrationCommands } from "./commands/orchestration/index.js";
|
|
16
17
|
import { registerRevenueOrganizationCommands } from "./commands/revenueOrganization/index.js";
|
|
17
18
|
import { ExitCodes, failWith } from "./commands/runHandler.js";
|
|
@@ -73,9 +74,13 @@ registerSystemOfRecordIntegrationCommands(program, getApi);
|
|
|
73
74
|
registerUserManagementCommands(program, getApi);
|
|
74
75
|
registerAiCommands(program, getApi);
|
|
75
76
|
registerHostingCommands(program, getApi);
|
|
77
|
+
registerMcpCommand(program, getApi);
|
|
76
78
|
registerCdkCommands(program
|
|
77
79
|
.command("cdk")
|
|
78
80
|
.description("Cargo CDK — define resources in code and deploy them (plan/deploy)"), getApi);
|
|
81
|
+
registerManifestCommands(program
|
|
82
|
+
.command("manifest")
|
|
83
|
+
.description("Manifest — scaffold a GTM repo (context, infra, skills, evals, outputs) and install cookbook modules into it"));
|
|
79
84
|
program
|
|
80
85
|
.parseAsync()
|
|
81
86
|
.then(() => maybeNotifyUpdate(version))
|
package/build/proxy.d.ts
CHANGED
|
@@ -14,4 +14,16 @@ import type { ClientTransport } from "@cargo-ai/api";
|
|
|
14
14
|
* default direct-egress behavior.
|
|
15
15
|
*/
|
|
16
16
|
export declare const getProxyTransport: (targetUrl: string) => ClientTransport | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Build a `fetch` implementation that tunnels through an HTTP/HTTPS proxy
|
|
19
|
+
* when one is configured via the same env vars as {@link getProxyTransport}.
|
|
20
|
+
*
|
|
21
|
+
* Node's built-in `fetch` (and therefore MCP's `StreamableHTTPClientTransport`)
|
|
22
|
+
* does not honor `HTTPS_PROXY` / `HTTP_PROXY` on its own. Callers that need
|
|
23
|
+
* proxied egress should pass the result as the transport's `fetch` option.
|
|
24
|
+
*
|
|
25
|
+
* Returns `undefined` when no proxy applies, so the caller can keep the
|
|
26
|
+
* default global `fetch`.
|
|
27
|
+
*/
|
|
28
|
+
export declare const getProxyFetch: (targetUrl: string) => ((url: string | URL, init?: RequestInit) => Promise<Response>) | undefined;
|
|
17
29
|
//# sourceMappingURL=proxy.d.ts.map
|
package/build/proxy.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAmErD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,iBAAiB,cACjB,MAAM,KAChB,eAAe,GAAG,SAWpB,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,aAAa,cACb,MAAM,YAER,MAAM,GAAG,GAAG,SAAS,WAAW,KAAK,QAAQ,QAAQ,CAAC,aAchE,CAAC"}
|
package/build/proxy.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { HttpProxyAgent } from "http-proxy-agent";
|
|
2
2
|
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
3
|
+
import { fetch as undiciFetch, ProxyAgent, } from "undici";
|
|
3
4
|
const getEnv = (...names) => {
|
|
4
5
|
for (const name of names) {
|
|
5
6
|
const value = process.env[name];
|
|
@@ -32,6 +33,21 @@ const shouldBypassProxy = (targetUrl, noProxy) => {
|
|
|
32
33
|
return hostname === normalized || hostname.endsWith(`.${normalized}`);
|
|
33
34
|
});
|
|
34
35
|
};
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the proxy URL for a target from the standard proxy env vars
|
|
38
|
+
* (`HTTPS_PROXY`, `HTTP_PROXY`, `ALL_PROXY`, honoring `NO_PROXY`).
|
|
39
|
+
*
|
|
40
|
+
* Returns `undefined` when no proxy applies.
|
|
41
|
+
*/
|
|
42
|
+
const resolveProxyUrl = (targetUrl) => {
|
|
43
|
+
if (shouldBypassProxy(targetUrl, getEnv("NO_PROXY", "no_proxy"))) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const isHttps = targetUrl.startsWith("https:");
|
|
47
|
+
return isHttps
|
|
48
|
+
? getEnv("HTTPS_PROXY", "https_proxy", "ALL_PROXY", "all_proxy")
|
|
49
|
+
: getEnv("HTTP_PROXY", "http_proxy", "ALL_PROXY", "all_proxy");
|
|
50
|
+
};
|
|
35
51
|
/**
|
|
36
52
|
* Build Axios transport overrides that tunnel requests through an
|
|
37
53
|
* HTTP/HTTPS proxy when one is configured via the standard proxy env
|
|
@@ -47,13 +63,7 @@ const shouldBypassProxy = (targetUrl, noProxy) => {
|
|
|
47
63
|
* default direct-egress behavior.
|
|
48
64
|
*/
|
|
49
65
|
export const getProxyTransport = (targetUrl) => {
|
|
50
|
-
|
|
51
|
-
return undefined;
|
|
52
|
-
}
|
|
53
|
-
const isHttps = targetUrl.startsWith("https:");
|
|
54
|
-
const proxyUrl = isHttps
|
|
55
|
-
? getEnv("HTTPS_PROXY", "https_proxy", "ALL_PROXY", "all_proxy")
|
|
56
|
-
: getEnv("HTTP_PROXY", "http_proxy", "ALL_PROXY", "all_proxy");
|
|
66
|
+
const proxyUrl = resolveProxyUrl(targetUrl);
|
|
57
67
|
if (proxyUrl === undefined) {
|
|
58
68
|
return undefined;
|
|
59
69
|
}
|
|
@@ -63,3 +73,25 @@ export const getProxyTransport = (targetUrl) => {
|
|
|
63
73
|
proxy: false,
|
|
64
74
|
};
|
|
65
75
|
};
|
|
76
|
+
/**
|
|
77
|
+
* Build a `fetch` implementation that tunnels through an HTTP/HTTPS proxy
|
|
78
|
+
* when one is configured via the same env vars as {@link getProxyTransport}.
|
|
79
|
+
*
|
|
80
|
+
* Node's built-in `fetch` (and therefore MCP's `StreamableHTTPClientTransport`)
|
|
81
|
+
* does not honor `HTTPS_PROXY` / `HTTP_PROXY` on its own. Callers that need
|
|
82
|
+
* proxied egress should pass the result as the transport's `fetch` option.
|
|
83
|
+
*
|
|
84
|
+
* Returns `undefined` when no proxy applies, so the caller can keep the
|
|
85
|
+
* default global `fetch`.
|
|
86
|
+
*/
|
|
87
|
+
export const getProxyFetch = (targetUrl) => {
|
|
88
|
+
const proxyUrl = resolveProxyUrl(targetUrl);
|
|
89
|
+
if (proxyUrl === undefined) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const agent = new ProxyAgent(proxyUrl);
|
|
93
|
+
return (url, init) => undiciFetch(url, {
|
|
94
|
+
...init,
|
|
95
|
+
dispatcher: agent,
|
|
96
|
+
});
|
|
97
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cargo-ai/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.32",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"description": "Command-line interface for the Cargo API",
|
|
@@ -31,14 +31,16 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@cargo-ai/api": "^1.0.40",
|
|
34
|
-
"@cargo-ai/
|
|
35
|
-
"@cargo-ai/app-sdk": "^1.0.3",
|
|
34
|
+
"@cargo-ai/app-sdk": "^1.0.5",
|
|
36
35
|
"@cargo-ai/cdk": "*",
|
|
36
|
+
"@cargo-ai/types": "*",
|
|
37
37
|
"@cargo-ai/worker-sdk": "^1.0.6",
|
|
38
|
+
"@modelcontextprotocol/sdk": "1.29.0",
|
|
38
39
|
"commander": "^12.1.0",
|
|
39
40
|
"http-proxy-agent": "^9.1.0",
|
|
40
41
|
"https-proxy-agent": "^9.1.0",
|
|
41
|
-
"tsx": "^4.19.2"
|
|
42
|
+
"tsx": "^4.19.2",
|
|
43
|
+
"undici": "^7.24.8"
|
|
42
44
|
},
|
|
43
45
|
"devDependencies": {
|
|
44
46
|
"@cargo-ai/eslint-config": "*",
|