@opengeni/ogtool 0.1.0 → 0.3.5

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/dist/cli.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { type AttemptToolCatalog, type AttemptToolCatalogEntry } from "@opengeni/codemode";
2
+ export declare function resolveTool(catalog: AttemptToolCatalog, name: string): AttemptToolCatalogEntry;
3
+ export declare function listProjection(catalog: AttemptToolCatalog): Record<string, unknown>;
4
+ export declare function parseToolArguments(raw: string | undefined): Record<string, unknown>;
package/dist/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  /** npm package name used by deployment BOMs and custom-rig bootstrap hints. */
2
- declare const OGTOOL_PACKAGE_NAME: "@opengeni/ogtool";
3
- /** Environment variables consumed by the dependency-free CLI. */
4
- declare const OGTOOL_ENVIRONMENT: {
5
- readonly url: "OPENGENI_TOOLSPACE_URL";
6
- readonly tokenFile: "OPENGENI_TOOLSPACE_TOKEN_FILE";
2
+ export declare const OGTOOL_PACKAGE_NAME: "@opengeni/ogtool";
3
+ /** Environment variables consumed by the standalone bundled CLI. */
4
+ export declare const OGTOOL_ENVIRONMENT: {
5
+ readonly url: "OPENGENI_CODEMODE_URL";
6
+ readonly token: "OPENGENI_CODEMODE_TOKEN";
7
+ readonly tokenFile: "OPENGENI_CODEMODE_TOKEN_FILE";
7
8
  readonly packageSpec: "OPENGENI_OGTOOL_PACKAGE_SPEC";
8
9
  };
9
-
10
- export { OGTOOL_ENVIRONMENT, OGTOOL_PACKAGE_NAME };
10
+ export * from "@opengeni/codemode";
package/dist/index.js CHANGED
@@ -1,8 +1,10 @@
1
1
  // src/index.ts
2
+ export * from "@opengeni/codemode";
2
3
  var OGTOOL_PACKAGE_NAME = "@opengeni/ogtool";
3
4
  var OGTOOL_ENVIRONMENT = {
4
- url: "OPENGENI_TOOLSPACE_URL",
5
- tokenFile: "OPENGENI_TOOLSPACE_TOKEN_FILE",
5
+ url: "OPENGENI_CODEMODE_URL",
6
+ token: "OPENGENI_CODEMODE_TOKEN",
7
+ tokenFile: "OPENGENI_CODEMODE_TOKEN_FILE",
6
8
  packageSpec: "OPENGENI_OGTOOL_PACKAGE_SPEC"
7
9
  };
8
10
  export {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/** npm package name used by deployment BOMs and custom-rig bootstrap hints. */\nexport const OGTOOL_PACKAGE_NAME = \"@opengeni/ogtool\" as const;\n\n/** Environment variables consumed by the dependency-free CLI. */\nexport const OGTOOL_ENVIRONMENT = {\n url: \"OPENGENI_TOOLSPACE_URL\",\n tokenFile: \"OPENGENI_TOOLSPACE_TOKEN_FILE\",\n packageSpec: \"OPENGENI_OGTOOL_PACKAGE_SPEC\",\n} as const;\n"],"mappings":";AACO,IAAM,sBAAsB;AAG5B,IAAM,qBAAqB;AAAA,EAChC,KAAK;AAAA,EACL,WAAW;AAAA,EACX,aAAa;AACf;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/** npm package name used by deployment BOMs and custom-rig bootstrap hints. */\nexport const OGTOOL_PACKAGE_NAME = \"@opengeni/ogtool\" as const;\n\n/** Environment variables consumed by the standalone bundled CLI. */\nexport const OGTOOL_ENVIRONMENT = {\n url: \"OPENGENI_CODEMODE_URL\",\n token: \"OPENGENI_CODEMODE_TOKEN\",\n tokenFile: \"OPENGENI_CODEMODE_TOKEN_FILE\",\n packageSpec: \"OPENGENI_OGTOOL_PACKAGE_SPEC\",\n} as const;\n\nexport * from \"@opengeni/codemode\";\n"],"mappings":";AAWA,cAAc;AAVP,IAAM,sBAAsB;AAG5B,IAAM,qBAAqB;AAAA,EAChC,KAAK;AAAA,EACL,OAAO;AAAA,EACP,WAAW;AAAA,EACX,aAAa;AACf;","names":[]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@opengeni/ogtool",
3
- "version": "0.1.0",
4
- "description": "Dependency-free CLI for calling the OpenGeni Toolspace MCP surface from a sandbox or custom rig.",
3
+ "version": "0.3.5",
4
+ "description": "Codemode CLI and typed client entrypoint for an OpenGeni execution attempt.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -9,10 +9,9 @@
9
9
  "directory": "packages/ogtool"
10
10
  },
11
11
  "bin": {
12
- "ogtool": "./bin/ogtool.cjs"
12
+ "ogtool": "./dist/bin/ogtool.cjs"
13
13
  },
14
14
  "files": [
15
- "bin",
16
15
  "dist",
17
16
  "src"
18
17
  ],
@@ -32,11 +31,14 @@
32
31
  "provenance": true
33
32
  },
34
33
  "scripts": {
35
- "typecheck": "tsgo --noEmit",
34
+ "typecheck": "tsc --noEmit",
36
35
  "test": "bun test",
37
- "build": "tsup",
36
+ "build": "bun build.ts",
38
37
  "prepublishOnly": "bash ../../scripts/prepublish-guard"
39
38
  },
39
+ "dependencies": {
40
+ "@opengeni/codemode": "^0.4.1"
41
+ },
40
42
  "engines": {
41
43
  "node": ">=18"
42
44
  }
package/src/cli.ts ADDED
@@ -0,0 +1,216 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { readFileSync, statSync, writeFileSync } from "node:fs";
3
+ import {
4
+ CodemodeClient,
5
+ generateCodemodeDeclarations,
6
+ type AttemptToolCatalog,
7
+ type AttemptToolCatalogEntry,
8
+ } from "@opengeni/codemode";
9
+ import packageManifest from "../package.json" with { type: "json" };
10
+
11
+ const PACKAGE_NAME = "@opengeni/ogtool";
12
+ const VERSION = packageManifest.version;
13
+
14
+ function usage(exitCode = 1): void {
15
+ const output = [
16
+ "usage:",
17
+ " ogtool list",
18
+ " ogtool call <tool-path-or-model-name> [json-object]",
19
+ " ogtool declarations [output-file]",
20
+ " ogtool doctor",
21
+ " ogtool --version",
22
+ "",
23
+ "requires OPENGENI_CODEMODE_URL and OPENGENI_CODEMODE_TOKEN or OPENGENI_CODEMODE_TOKEN_FILE",
24
+ ].join("\n");
25
+ (exitCode === 0 ? process.stdout : process.stderr).write(`${output}\n`);
26
+ process.exitCode = exitCode;
27
+ }
28
+
29
+ function configuredClient(): CodemodeClient {
30
+ const baseUrl = process.env.OPENGENI_CODEMODE_URL?.trim();
31
+ const directTokenConfigured = process.env.OPENGENI_CODEMODE_TOKEN !== undefined;
32
+ const tokenFile = process.env.OPENGENI_CODEMODE_TOKEN_FILE?.trim();
33
+ if (!baseUrl) throw new Error("OPENGENI_CODEMODE_URL is required");
34
+ if (!directTokenConfigured && !tokenFile) {
35
+ throw new Error("OPENGENI_CODEMODE_TOKEN or OPENGENI_CODEMODE_TOKEN_FILE is required");
36
+ }
37
+ return new CodemodeClient({
38
+ baseUrl,
39
+ token: directTokenConfigured
40
+ ? async () => requiredDirectToken()
41
+ : async () => readToken(tokenFile!),
42
+ });
43
+ }
44
+
45
+ function requiredDirectToken(): string {
46
+ const token = process.env.OPENGENI_CODEMODE_TOKEN?.trim();
47
+ if (!token) throw new Error("OPENGENI_CODEMODE_TOKEN is empty");
48
+ return token;
49
+ }
50
+
51
+ function readToken(tokenFile: string): string {
52
+ let token: string;
53
+ try {
54
+ token = readFileSync(tokenFile, "utf8").trim();
55
+ } catch {
56
+ throw new Error("OPENGENI_CODEMODE_TOKEN_FILE is not readable");
57
+ }
58
+ if (!token) throw new Error("OPENGENI_CODEMODE_TOKEN_FILE is empty");
59
+ return token;
60
+ }
61
+
62
+ function doctor(): void {
63
+ const directTokenRaw = process.env.OPENGENI_CODEMODE_TOKEN;
64
+ const directTokenConfigured = directTokenRaw !== undefined;
65
+ const directTokenNonempty = Boolean(directTokenRaw?.trim().length);
66
+ const tokenFile = process.env.OPENGENI_CODEMODE_TOKEN_FILE ?? null;
67
+ const rawUrl = process.env.OPENGENI_CODEMODE_URL ?? null;
68
+ let urlValid = false;
69
+ let protocol: string | null = null;
70
+ if (rawUrl) {
71
+ try {
72
+ const target = new URL(rawUrl);
73
+ protocol = target.protocol;
74
+ urlValid = protocol === "http:" || protocol === "https:";
75
+ } catch {
76
+ urlValid = false;
77
+ }
78
+ }
79
+ let tokenFileReadable = false;
80
+ let tokenFileNonempty = false;
81
+ if (tokenFile) {
82
+ try {
83
+ tokenFileReadable = statSync(tokenFile).isFile();
84
+ tokenFileNonempty = tokenFileReadable && readFileSync(tokenFile, "utf8").trim().length > 0;
85
+ } catch {
86
+ tokenFileReadable = false;
87
+ }
88
+ }
89
+ const tokenMode = directTokenConfigured ? "environment" : tokenFile ? "file" : null;
90
+ const tokenReady = directTokenConfigured ? directTokenNonempty : tokenFileNonempty;
91
+ const ok = urlValid && tokenReady;
92
+ process.stdout.write(
93
+ `${JSON.stringify(
94
+ {
95
+ ok,
96
+ package: PACKAGE_NAME,
97
+ version: VERSION,
98
+ node: process.version,
99
+ urlConfigured: rawUrl !== null,
100
+ urlValid,
101
+ protocol,
102
+ tokenMode,
103
+ directTokenConfigured,
104
+ directTokenNonempty,
105
+ tokenFileConfigured: tokenFile !== null,
106
+ tokenFileReadable,
107
+ tokenFileNonempty,
108
+ packageSpec: process.env.OPENGENI_OGTOOL_PACKAGE_SPEC ?? null,
109
+ },
110
+ null,
111
+ 2,
112
+ )}\n`,
113
+ );
114
+ process.exitCode = ok ? 0 : 1;
115
+ }
116
+
117
+ export function resolveTool(catalog: AttemptToolCatalog, name: string): AttemptToolCatalogEntry {
118
+ const matches = catalog.entries.filter(
119
+ (entry) =>
120
+ entry.modelName === name ||
121
+ entry.codemodePath.join(".") === name ||
122
+ `${entry.identity.serverId}.${entry.identity.toolName}` === name,
123
+ );
124
+ if (matches.length !== 1) {
125
+ throw new Error(
126
+ matches.length === 0 ? `Unknown Codemode tool: ${name}` : `Ambiguous tool: ${name}`,
127
+ );
128
+ }
129
+ return matches[0]!;
130
+ }
131
+
132
+ export function listProjection(catalog: AttemptToolCatalog): Record<string, unknown> {
133
+ return {
134
+ catalogDigest: catalog.digest,
135
+ attemptId: catalog.attemptId,
136
+ tools: catalog.entries.map((entry) => ({
137
+ name: entry.modelName,
138
+ path: entry.codemodePath.join("."),
139
+ identity: entry.identity,
140
+ ...(entry.title ? { title: entry.title } : {}),
141
+ ...(entry.description ? { description: entry.description } : {}),
142
+ inputSchema: entry.inputSchema,
143
+ ...(entry.outputSchema ? { outputSchema: entry.outputSchema } : {}),
144
+ ...(entry.annotations ? { annotations: entry.annotations } : {}),
145
+ approval: entry.approval,
146
+ source: entry.source,
147
+ })),
148
+ };
149
+ }
150
+
151
+ export function parseToolArguments(raw: string | undefined): Record<string, unknown> {
152
+ if (raw === undefined) return {};
153
+ let value: unknown;
154
+ try {
155
+ value = JSON.parse(raw);
156
+ } catch {
157
+ throw new Error("tool arguments must be valid JSON");
158
+ }
159
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
160
+ throw new Error("tool arguments must be a JSON object");
161
+ }
162
+ return value as Record<string, unknown>;
163
+ }
164
+
165
+ async function main(): Promise<void> {
166
+ const command = process.argv[2];
167
+ if (command === "--help" || command === "-h" || command === "help") return usage(0);
168
+ if (command === "--version" || command === "-V" || command === "version") {
169
+ process.stdout.write(`${VERSION}\n`);
170
+ return;
171
+ }
172
+ if (command === "doctor") return doctor();
173
+ if (!command) return usage();
174
+
175
+ const client = configuredClient();
176
+ if (command === "list") {
177
+ process.stdout.write(`${JSON.stringify(listProjection(await client.catalog()), null, 2)}\n`);
178
+ return;
179
+ }
180
+ if (command === "call") {
181
+ const name = process.argv[3];
182
+ if (!name) throw new Error("tool path or model name is required");
183
+ const argumentsValue = parseToolArguments(process.argv[4]);
184
+ const catalog = await client.catalog();
185
+ const entry = resolveTool(catalog, name);
186
+ const result = await client.call(entry.identity, argumentsValue, {
187
+ operationId: randomUUID(),
188
+ });
189
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
190
+ return;
191
+ }
192
+ if (command === "declarations") {
193
+ const catalog = await client.catalog();
194
+ const declaration = generateCodemodeDeclarations(catalog);
195
+ const output = process.argv[3];
196
+ if (!output || output === "-") {
197
+ process.stdout.write(declaration);
198
+ } else {
199
+ writeFileSync(output, declaration, { encoding: "utf8", mode: 0o600 });
200
+ process.stdout.write(
201
+ `${JSON.stringify({ catalogDigest: catalog.digest, output }, null, 2)}\n`,
202
+ );
203
+ }
204
+ return;
205
+ }
206
+ return usage();
207
+ }
208
+
209
+ void main().catch((error: unknown) => {
210
+ const code =
211
+ error && typeof error === "object" && "code" in error
212
+ ? String((error as { code: unknown }).code)
213
+ : null;
214
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
215
+ process.exitCode = code === "tool_outcome_unknown" ? 3 : 1;
216
+ });
package/src/index.ts CHANGED
@@ -1,9 +1,12 @@
1
1
  /** npm package name used by deployment BOMs and custom-rig bootstrap hints. */
2
2
  export const OGTOOL_PACKAGE_NAME = "@opengeni/ogtool" as const;
3
3
 
4
- /** Environment variables consumed by the dependency-free CLI. */
4
+ /** Environment variables consumed by the standalone bundled CLI. */
5
5
  export const OGTOOL_ENVIRONMENT = {
6
- url: "OPENGENI_TOOLSPACE_URL",
7
- tokenFile: "OPENGENI_TOOLSPACE_TOKEN_FILE",
6
+ url: "OPENGENI_CODEMODE_URL",
7
+ token: "OPENGENI_CODEMODE_TOKEN",
8
+ tokenFile: "OPENGENI_CODEMODE_TOKEN_FILE",
8
9
  packageSpec: "OPENGENI_OGTOOL_PACKAGE_SPEC",
9
10
  } as const;
11
+
12
+ export * from "@opengeni/codemode";
package/bin/ogtool.cjs DELETED
@@ -1,327 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- "use strict";
4
-
5
- const fs = require("node:fs");
6
- const http = require("node:http");
7
- const https = require("node:https");
8
- const path = require("node:path");
9
-
10
- const PACKAGE_NAME = "@opengeni/ogtool";
11
- const DEFAULT_PROTOCOL_VERSION = "2025-03-26";
12
- const DEFAULT_MAX_RESPONSE_BYTES = 64 * 1024 * 1024;
13
- const DEFAULT_TIMEOUT_MS = 120_000;
14
-
15
- function packageVersion() {
16
- try {
17
- const manifest = JSON.parse(
18
- fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"),
19
- );
20
- if (typeof manifest.version === "string" && manifest.version.length > 0) {
21
- return manifest.version;
22
- }
23
- } catch {
24
- // A malformed/custom installation is reported honestly by --version and
25
- // doctor rather than blocking the direct MCP fallback.
26
- }
27
- return "unknown";
28
- }
29
-
30
- const VERSION = packageVersion();
31
-
32
- function usage(exitCode = 1) {
33
- const out = [
34
- "usage:",
35
- " ogtool list",
36
- " ogtool call <tool-name> [json-object]",
37
- " ogtool doctor",
38
- " ogtool --version",
39
- "",
40
- "aliases: tools/list, tools/call",
41
- "requires OPENGENI_TOOLSPACE_TOKEN_FILE and OPENGENI_TOOLSPACE_URL for list/call",
42
- ].join("\n");
43
- (exitCode === 0 ? process.stdout : process.stderr).write(`${out}\n`);
44
- process.exitCode = exitCode;
45
- }
46
-
47
- function positiveIntegerEnvironment(name, fallback) {
48
- const raw = process.env[name];
49
- if (raw === undefined || raw === "") return fallback;
50
- const parsed = Number(raw);
51
- if (!Number.isSafeInteger(parsed) || parsed <= 0) {
52
- throw new Error(`${name} must be a positive integer`);
53
- }
54
- return parsed;
55
- }
56
-
57
- function configuredEndpoint() {
58
- const raw = process.env.OPENGENI_TOOLSPACE_URL;
59
- if (!raw) throw new Error("OPENGENI_TOOLSPACE_URL is required");
60
- let target;
61
- try {
62
- target = new URL(raw);
63
- } catch {
64
- throw new Error("OPENGENI_TOOLSPACE_URL must be a valid URL");
65
- }
66
- if (target.protocol !== "http:" && target.protocol !== "https:") {
67
- throw new Error("OPENGENI_TOOLSPACE_URL must use http or https");
68
- }
69
- return target;
70
- }
71
-
72
- function readToken() {
73
- const tokenFile = process.env.OPENGENI_TOOLSPACE_TOKEN_FILE;
74
- if (!tokenFile) throw new Error("OPENGENI_TOOLSPACE_TOKEN_FILE is required");
75
- let token;
76
- try {
77
- token = fs.readFileSync(tokenFile, "utf8").trim();
78
- } catch {
79
- throw new Error("OPENGENI_TOOLSPACE_TOKEN_FILE is not readable");
80
- }
81
- if (!token) throw new Error("OPENGENI_TOOLSPACE_TOKEN_FILE is empty");
82
- return token;
83
- }
84
-
85
- function doctor() {
86
- const tokenFile = process.env.OPENGENI_TOOLSPACE_TOKEN_FILE ?? null;
87
- const rawUrl = process.env.OPENGENI_TOOLSPACE_URL ?? null;
88
- let urlValid = false;
89
- let protocol = null;
90
- if (rawUrl) {
91
- try {
92
- const target = new URL(rawUrl);
93
- protocol = target.protocol;
94
- urlValid = protocol === "http:" || protocol === "https:";
95
- } catch {
96
- urlValid = false;
97
- }
98
- }
99
- let tokenFileReadable = false;
100
- let tokenFileNonempty = false;
101
- if (tokenFile) {
102
- try {
103
- tokenFileReadable = fs.statSync(tokenFile).isFile();
104
- tokenFileNonempty = tokenFileReadable && fs.readFileSync(tokenFile, "utf8").trim().length > 0;
105
- } catch {
106
- tokenFileReadable = false;
107
- }
108
- }
109
- const ok = urlValid && tokenFileReadable && tokenFileNonempty;
110
- process.stdout.write(
111
- `${JSON.stringify(
112
- {
113
- ok,
114
- package: PACKAGE_NAME,
115
- version: VERSION,
116
- node: process.version,
117
- urlConfigured: rawUrl !== null,
118
- urlValid,
119
- protocol,
120
- tokenFileConfigured: tokenFile !== null,
121
- tokenFileReadable,
122
- tokenFileNonempty,
123
- packageSpec: process.env.OPENGENI_OGTOOL_PACKAGE_SPEC ?? null,
124
- },
125
- null,
126
- 2,
127
- )}\n`,
128
- );
129
- process.exitCode = ok ? 0 : 1;
130
- }
131
-
132
- function parseEventStream(text) {
133
- const messages = [];
134
- for (const block of text.split(/\r?\n\r?\n/u)) {
135
- const data = block
136
- .split(/\r?\n/u)
137
- .filter((line) => line.startsWith("data:"))
138
- .map((line) => line.slice(5).trimStart())
139
- .join("\n");
140
- if (!data || data === "[DONE]") continue;
141
- try {
142
- messages.push(JSON.parse(data));
143
- } catch {
144
- throw new Error("Toolspace returned an invalid event-stream JSON message");
145
- }
146
- }
147
- return messages.at(-1) ?? null;
148
- }
149
-
150
- function parseResponseBody(text, contentType) {
151
- if (!text) return null;
152
- if (contentType.toLowerCase().includes("text/event-stream")) {
153
- return parseEventStream(text);
154
- }
155
- try {
156
- return JSON.parse(text);
157
- } catch {
158
- throw new Error("Toolspace returned a non-JSON response");
159
- }
160
- }
161
-
162
- class McpClient {
163
- constructor(target, token) {
164
- this.target = target;
165
- this.token = token;
166
- this.nextId = 1;
167
- this.sessionId = null;
168
- this.protocolVersion = DEFAULT_PROTOCOL_VERSION;
169
- this.maxResponseBytes = positiveIntegerEnvironment(
170
- "OPENGENI_OGTOOL_MAX_RESPONSE_BYTES",
171
- DEFAULT_MAX_RESPONSE_BYTES,
172
- );
173
- this.timeoutMs = positiveIntegerEnvironment("OPENGENI_OGTOOL_TIMEOUT_MS", DEFAULT_TIMEOUT_MS);
174
- }
175
-
176
- rpc(method, params) {
177
- return {
178
- jsonrpc: "2.0",
179
- id: this.nextId++,
180
- method,
181
- ...(params === undefined ? {} : { params }),
182
- };
183
- }
184
-
185
- notify(method, params) {
186
- return {
187
- jsonrpc: "2.0",
188
- method,
189
- ...(params === undefined ? {} : { params }),
190
- };
191
- }
192
-
193
- request(payload) {
194
- const body = JSON.stringify(payload);
195
- const transport = this.target.protocol === "https:" ? https : http;
196
- const headers = {
197
- accept: "application/json, text/event-stream",
198
- authorization: `Bearer ${this.token}`,
199
- "content-type": "application/json",
200
- "content-length": Buffer.byteLength(body),
201
- "mcp-protocol-version": this.protocolVersion,
202
- };
203
- if (this.sessionId) headers["mcp-session-id"] = this.sessionId;
204
-
205
- return new Promise((resolve, reject) => {
206
- const request = transport.request(
207
- this.target,
208
- { method: "POST", headers, timeout: this.timeoutMs },
209
- (response) => {
210
- const chunks = [];
211
- let received = 0;
212
- response.on("data", (chunk) => {
213
- received += chunk.length;
214
- if (received > this.maxResponseBytes) {
215
- response.destroy(
216
- new Error(`Toolspace response exceeds ${this.maxResponseBytes} bytes`),
217
- );
218
- return;
219
- }
220
- chunks.push(chunk);
221
- });
222
- response.on("error", reject);
223
- response.on("end", () => {
224
- const returnedSession = response.headers["mcp-session-id"];
225
- if (typeof returnedSession === "string" && returnedSession) {
226
- this.sessionId = returnedSession;
227
- }
228
- const text = Buffer.concat(chunks).toString("utf8");
229
- const status = response.statusCode ?? 0;
230
- if (status < 200 || status >= 300) {
231
- const preview = text.replace(/\s+/gu, " ").trim().slice(0, 1_000);
232
- reject(new Error(`Toolspace HTTP ${status}${preview ? `: ${preview}` : ""}`));
233
- return;
234
- }
235
- try {
236
- resolve(parseResponseBody(text, String(response.headers["content-type"] ?? "")));
237
- } catch (error) {
238
- reject(error);
239
- }
240
- });
241
- },
242
- );
243
- request.on("timeout", () => request.destroy(new Error("Toolspace request timed out")));
244
- request.on("error", reject);
245
- request.end(body);
246
- });
247
- }
248
-
249
- async initialize() {
250
- const initialized = await this.request(
251
- this.rpc("initialize", {
252
- protocolVersion: DEFAULT_PROTOCOL_VERSION,
253
- capabilities: {},
254
- clientInfo: { name: "ogtool", version: VERSION },
255
- }),
256
- );
257
- const negotiated = initialized?.result?.protocolVersion;
258
- if (typeof negotiated === "string" && negotiated) this.protocolVersion = negotiated;
259
- await this.request(this.notify("notifications/initialized"));
260
- }
261
- }
262
-
263
- function parseToolArguments(raw) {
264
- if (raw === undefined) return {};
265
- let value;
266
- try {
267
- value = JSON.parse(raw);
268
- } catch {
269
- throw new Error("tool arguments must be valid JSON");
270
- }
271
- if (!value || typeof value !== "object" || Array.isArray(value)) {
272
- throw new Error("tool arguments must be a JSON object");
273
- }
274
- return value;
275
- }
276
-
277
- async function main() {
278
- const command = process.argv[2];
279
- if (command === "--help" || command === "-h" || command === "help") {
280
- usage(0);
281
- return;
282
- }
283
- if (command === "--version" || command === "-V" || command === "version") {
284
- process.stdout.write(`${VERSION}\n`);
285
- return;
286
- }
287
- if (command === "doctor") {
288
- doctor();
289
- return;
290
- }
291
- if (!command) {
292
- usage();
293
- return;
294
- }
295
-
296
- const client = new McpClient(configuredEndpoint(), readToken());
297
- await client.initialize();
298
- let response;
299
- if (command === "list" || command === "tools/list") {
300
- response = await client.request(client.rpc("tools/list", {}));
301
- } else if (command === "call" || command === "tools/call") {
302
- const name = process.argv[3];
303
- if (!name) throw new Error("tool name is required");
304
- response = await client.request(
305
- client.rpc("tools/call", { name, arguments: parseToolArguments(process.argv[4]) }),
306
- );
307
- } else {
308
- usage();
309
- return;
310
- }
311
-
312
- if (response?.error) {
313
- process.stderr.write(`${JSON.stringify(response, null, 2)}\n`);
314
- process.exitCode = 2;
315
- return;
316
- }
317
- process.stdout.write(`${JSON.stringify(response?.result ?? response, null, 2)}\n`);
318
- }
319
-
320
- if (require.main === module) {
321
- main().catch((error) => {
322
- process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
323
- process.exitCode = 1;
324
- });
325
- }
326
-
327
- module.exports = { McpClient, parseEventStream, parseResponseBody, parseToolArguments };