@cargo-ai/cli 1.0.31 → 1.0.33

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.
@@ -0,0 +1,4 @@
1
+ import type { Command } from "commander";
2
+ import type { Api } from "../api.js";
3
+ export declare function registerMcpCommand(parent: Command, getApi: () => Api): void;
4
+ //# sourceMappingURL=mcp.d.ts.map
@@ -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
@@ -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;AA4CrD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,iBAAiB,cACjB,MAAM,KAChB,eAAe,GAAG,SAoBpB,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
- if (shouldBypassProxy(targetUrl, getEnv("NO_PROXY", "no_proxy"))) {
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.31",
3
+ "version": "1.0.33",
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/types": "*",
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": "*",