tg-agent 0.1.0 → 1.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.
package/dist/mcp.js CHANGED
@@ -1,427 +1,3 @@
1
- import fs from "node:fs";
2
- import fsPromises from "node:fs/promises";
3
- import path from "node:path";
4
- import readline from "node:readline";
5
- import { spawn } from "node:child_process";
6
- const DEFAULT_TIMEOUT_MS = 60_000;
7
- const DEFAULT_MAX_BYTES = 200_000;
8
- export function getMcpConfigPath(agentDir) {
9
- return path.join(agentDir, "config.toml");
10
- }
11
- export function loadMcpServersSync(agentDir) {
12
- const configPath = getMcpConfigPath(agentDir);
13
- try {
14
- const content = fs.readFileSync(configPath, "utf8");
15
- return parseMcpToml(content);
16
- }
17
- catch {
18
- return [];
19
- }
20
- }
21
- export async function loadMcpServers(agentDir) {
22
- const configPath = getMcpConfigPath(agentDir);
23
- try {
24
- const content = await fsPromises.readFile(configPath, "utf8");
25
- return parseMcpToml(content);
26
- }
27
- catch {
28
- return [];
29
- }
30
- }
31
- export function formatMcpTarget(server) {
32
- if (server.type === "http") {
33
- return server.url ? `url=${server.url}` : "url=(missing)";
34
- }
35
- const args = server.args && server.args.length > 0 ? ` ${server.args.join(" ")}` : "";
36
- return server.command ? `command=${server.command}${args}` : "command=(missing)";
37
- }
38
- export async function probeMcpServer(server, timeoutMs = 5000) {
39
- const started = Date.now();
40
- try {
41
- await callMcpServer(server, "tools/list", {}, { timeoutMs });
42
- return { ok: true, durationMs: Date.now() - started };
43
- }
44
- catch (error) {
45
- const message = error instanceof Error ? error.message : String(error);
46
- return { ok: false, durationMs: Date.now() - started, error: message };
47
- }
48
- }
49
- export async function callMcpServer(server, method, params, options = {}) {
50
- const timeoutMs = clampNumber(options.timeoutMs ?? DEFAULT_TIMEOUT_MS, 1000, 120_000);
51
- const maxBytes = clampNumber(options.maxBytes ?? DEFAULT_MAX_BYTES, 1024, 5_000_000);
52
- const controller = new AbortController();
53
- const onAbort = () => controller.abort();
54
- options.signal?.addEventListener("abort", onAbort, { once: true });
55
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
56
- try {
57
- if (server.type === "http") {
58
- return await callHttpMcp(server, method, params, maxBytes, controller.signal);
59
- }
60
- return await callStdioMcp(server, method, params, maxBytes, controller.signal);
61
- }
62
- finally {
63
- clearTimeout(timeout);
64
- options.signal?.removeEventListener("abort", onAbort);
65
- }
66
- }
67
- function clampNumber(value, min, max) {
68
- if (Number.isNaN(value))
69
- return min;
70
- return Math.min(max, Math.max(min, value));
71
- }
72
- async function callHttpMcp(server, method, params, maxBytes, signal) {
73
- if (!server.url) {
74
- throw new Error("MCP server url is missing.");
75
- }
76
- const payload = {
77
- jsonrpc: "2.0",
78
- id: `tg-agent-${Date.now()}`,
79
- method,
80
- params: params ?? {},
81
- };
82
- const headers = {
83
- "content-type": "application/json",
84
- };
85
- if (server.auth) {
86
- headers.authorization = server.auth.startsWith("Bearer ")
87
- ? server.auth
88
- : `Bearer ${server.auth}`;
89
- }
90
- const started = Date.now();
91
- const response = await fetch(server.url, {
92
- method: "POST",
93
- headers,
94
- body: JSON.stringify(payload),
95
- signal,
96
- });
97
- const readResult = await readResponseText(response, maxBytes, signal);
98
- const contentType = response.headers.get("content-type");
99
- let bodyText = readResult.text;
100
- try {
101
- const parsed = JSON.parse(readResult.text);
102
- bodyText = JSON.stringify(parsed, null, 2);
103
- }
104
- catch {
105
- // keep raw text
106
- }
107
- return {
108
- ok: response.ok,
109
- output: bodyText,
110
- durationMs: Date.now() - started,
111
- bytes: readResult.bytes,
112
- truncated: readResult.truncated,
113
- statusCode: response.status,
114
- statusText: response.statusText,
115
- contentType,
116
- };
117
- }
118
- async function callStdioMcp(server, method, params, maxBytes, signal) {
119
- if (!server.command) {
120
- throw new Error("MCP server command is missing.");
121
- }
122
- const payload = {
123
- jsonrpc: "2.0",
124
- id: `tg-agent-${Date.now()}`,
125
- method,
126
- params: params ?? {},
127
- };
128
- const started = Date.now();
129
- const child = spawn(server.command, server.args ?? [], {
130
- stdio: ["pipe", "pipe", "pipe"],
131
- });
132
- const cleanup = () => {
133
- if (!child.killed) {
134
- child.kill("SIGTERM");
135
- }
136
- };
137
- if (signal.aborted) {
138
- cleanup();
139
- throw new Error("MCP request aborted.");
140
- }
141
- signal.addEventListener("abort", cleanup, { once: true });
142
- let stdoutBuffer = "";
143
- let stderrBuffer = "";
144
- let truncated = false;
145
- let parsedJson;
146
- const stdoutRl = readline.createInterface({ input: child.stdout, terminal: false });
147
- stdoutRl.on("line", (line) => {
148
- if (stdoutBuffer.length + line.length + 1 > maxBytes) {
149
- truncated = true;
150
- return;
151
- }
152
- stdoutBuffer += line + "\n";
153
- if (parsedJson === undefined) {
154
- try {
155
- parsedJson = JSON.parse(line);
156
- }
157
- catch {
158
- // ignore non-json line
159
- }
160
- }
161
- });
162
- child.stderr?.on("data", (data) => {
163
- const text = data.toString();
164
- if (stderrBuffer.length + text.length > maxBytes) {
165
- truncated = true;
166
- return;
167
- }
168
- stderrBuffer += text;
169
- });
170
- const payloadLine = `${JSON.stringify(payload)}\n`;
171
- child.stdin?.write(payloadLine);
172
- child.stdin?.end();
173
- const exitResult = await new Promise((resolve) => {
174
- child.on("exit", (code, exitSignal) => resolve({ code, signal: exitSignal }));
175
- });
176
- stdoutRl.close();
177
- signal.removeEventListener("abort", cleanup);
178
- if (signal.aborted) {
179
- throw new Error("MCP request aborted.");
180
- }
181
- if (!parsedJson && stdoutBuffer.trim()) {
182
- try {
183
- parsedJson = JSON.parse(stdoutBuffer.trim());
184
- }
185
- catch {
186
- // ignore
187
- }
188
- }
189
- if (exitResult.code !== 0 && !parsedJson) {
190
- const stderrText = stderrBuffer.trim() || "stdio process failed";
191
- throw new Error(stderrText);
192
- }
193
- if (!parsedJson) {
194
- const stderrText = stderrBuffer.trim();
195
- if (stderrText) {
196
- throw new Error(stderrText);
197
- }
198
- throw new Error("MCP stdio returned no JSON.");
199
- }
200
- const output = JSON.stringify(parsedJson, null, 2);
201
- return {
202
- ok: true,
203
- output,
204
- durationMs: Date.now() - started,
205
- bytes: Math.min(output.length, maxBytes),
206
- truncated,
207
- };
208
- }
209
- async function readResponseText(response, maxBytes, signal) {
210
- const reader = response.body?.getReader?.();
211
- if (!reader) {
212
- const text = await response.text();
213
- const bytes = Math.min(text.length, maxBytes);
214
- return { text: text.slice(0, maxBytes), bytes, truncated: text.length > maxBytes };
215
- }
216
- const decoder = new TextDecoder("utf-8");
217
- const chunks = [];
218
- let bytes = 0;
219
- let truncated = false;
220
- while (true) {
221
- if (signal.aborted) {
222
- try {
223
- await reader.cancel();
224
- }
225
- catch {
226
- // ignore
227
- }
228
- throw new Error("MCP request aborted.");
229
- }
230
- const { done, value } = await reader.read();
231
- if (done)
232
- break;
233
- if (!value)
234
- continue;
235
- const nextBytes = bytes + value.byteLength;
236
- if (nextBytes > maxBytes) {
237
- const sliceSize = Math.max(0, maxBytes - bytes);
238
- if (sliceSize > 0) {
239
- chunks.push(value.slice(0, sliceSize));
240
- bytes += sliceSize;
241
- }
242
- truncated = true;
243
- try {
244
- await reader.cancel();
245
- }
246
- catch {
247
- // ignore
248
- }
249
- break;
250
- }
251
- chunks.push(value);
252
- bytes = nextBytes;
253
- }
254
- const buffer = new Uint8Array(bytes);
255
- let offset = 0;
256
- for (const chunk of chunks) {
257
- buffer.set(chunk, offset);
258
- offset += chunk.byteLength;
259
- }
260
- const text = decoder.decode(buffer);
261
- return { text, bytes, truncated };
262
- }
263
- function parseMcpToml(content) {
264
- const servers = [];
265
- let currentName = null;
266
- let current = {};
267
- const flush = () => {
268
- if (!currentName)
269
- return;
270
- const type = current.type ??
271
- (current.url ? "http" : current.command ? "stdio" : undefined);
272
- if (!type)
273
- return;
274
- const normalizedType = type === "http" || type === "stdio" ? type : undefined;
275
- if (!normalizedType)
276
- return;
277
- if (normalizedType === "http" && !current.url)
278
- return;
279
- if (normalizedType === "stdio" && !current.command)
280
- return;
281
- servers.push({
282
- name: currentName,
283
- type: normalizedType,
284
- url: current.url,
285
- command: current.command,
286
- args: current.args,
287
- auth: current.auth,
288
- });
289
- };
290
- for (const rawLine of content.split(/\r?\n/)) {
291
- const line = stripInlineComment(rawLine).trim();
292
- if (!line)
293
- continue;
294
- if (line.startsWith("[") && line.endsWith("]")) {
295
- flush();
296
- const section = line.slice(1, -1).trim();
297
- if (!section.startsWith("mcp_servers.")) {
298
- currentName = null;
299
- current = {};
300
- continue;
301
- }
302
- let name = section.slice("mcp_servers.".length).trim();
303
- name = stripQuotes(name);
304
- if (!name) {
305
- currentName = null;
306
- current = {};
307
- continue;
308
- }
309
- currentName = name;
310
- current = {};
311
- continue;
312
- }
313
- if (!currentName)
314
- continue;
315
- const eq = line.indexOf("=");
316
- if (eq === -1)
317
- continue;
318
- const key = line.slice(0, eq).trim();
319
- const value = line.slice(eq + 1).trim();
320
- if (!key)
321
- continue;
322
- switch (key) {
323
- case "type":
324
- current.type = parseTomlString(value).toLowerCase();
325
- break;
326
- case "url":
327
- current.url = parseTomlString(value);
328
- break;
329
- case "command":
330
- current.command = parseTomlString(value);
331
- break;
332
- case "args":
333
- current.args = parseTomlStringArray(value);
334
- break;
335
- case "auth":
336
- case "authorization":
337
- case "token":
338
- current.auth = parseTomlString(value);
339
- break;
340
- default:
341
- break;
342
- }
343
- }
344
- flush();
345
- return servers;
346
- }
347
- function stripQuotes(value) {
348
- if (value.startsWith("\"") && value.endsWith("\"")) {
349
- return value.slice(1, -1);
350
- }
351
- if (value.startsWith("'") && value.endsWith("'")) {
352
- return value.slice(1, -1);
353
- }
354
- return value;
355
- }
356
- function parseTomlString(value) {
357
- const trimmed = value.trim();
358
- if (!trimmed)
359
- return "";
360
- if (trimmed.startsWith("\"") && trimmed.endsWith("\"")) {
361
- return trimmed
362
- .slice(1, -1)
363
- .replace(/\\\\/g, "\\")
364
- .replace(/\\"/g, "\"");
365
- }
366
- if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
367
- return trimmed.slice(1, -1);
368
- }
369
- return trimmed;
370
- }
371
- function parseTomlStringArray(value) {
372
- const trimmed = value.trim();
373
- if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) {
374
- return [];
375
- }
376
- const inner = trimmed.slice(1, -1).trim();
377
- if (!inner)
378
- return [];
379
- const entries = [];
380
- let current = "";
381
- let inQuotes = false;
382
- let quoteChar = "";
383
- for (let i = 0; i < inner.length; i += 1) {
384
- const char = inner[i];
385
- if (!inQuotes && (char === "\"" || char === "'")) {
386
- inQuotes = true;
387
- quoteChar = char;
388
- continue;
389
- }
390
- if (inQuotes && char === quoteChar) {
391
- inQuotes = false;
392
- continue;
393
- }
394
- if (!inQuotes && char === ",") {
395
- const entry = current.trim();
396
- if (entry)
397
- entries.push(stripQuotes(entry));
398
- current = "";
399
- continue;
400
- }
401
- current += char;
402
- }
403
- const last = current.trim();
404
- if (last)
405
- entries.push(stripQuotes(last));
406
- return entries;
407
- }
408
- function stripInlineComment(line) {
409
- let inQuotes = false;
410
- let quoteChar = "";
411
- for (let i = 0; i < line.length; i += 1) {
412
- const char = line[i];
413
- if (!inQuotes && (char === "\"" || char === "'")) {
414
- inQuotes = true;
415
- quoteChar = char;
416
- continue;
417
- }
418
- if (inQuotes && char === quoteChar) {
419
- inQuotes = false;
420
- continue;
421
- }
422
- if (!inQuotes && (char === "#" || char === ";")) {
423
- return line.slice(0, i);
424
- }
425
- }
426
- return line;
427
- }
1
+ import E from"node:fs";import W from"node:fs/promises";import C from"node:path";import N from"node:readline";import{spawn as D}from"node:child_process";const P=6e4,L=2e5;function M(t){return C.join(t,"config.toml")}function U(t){const n=M(t);try{const o=E.readFileSync(n,"utf8");return x(o)}catch{return[]}}async function Q(t){const n=M(t);try{const o=await W.readFile(n,"utf8");return x(o)}catch{return[]}}function B(t){if(t.type==="http")return t.url?`url=${t.url}`:"url=(missing)";const n=t.args&&t.args.length>0?` ${t.args.join(" ")}`:"";return t.command?`command=${t.command}${n}`:"command=(missing)"}async function G(t,n=5e3){const o=Date.now();try{return await O(t,"tools/list",{},{timeoutMs:n}),{ok:!0,durationMs:Date.now()-o}}catch(e){const i=e instanceof Error?e.message:String(e);return{ok:!1,durationMs:Date.now()-o,error:i}}}async function O(t,n,o,e={}){const i=S(e.timeoutMs??P,1e3,12e4),a=S(e.maxBytes??L,1024,5e6),r=new AbortController,c=()=>r.abort();e.signal?.addEventListener("abort",c,{once:!0});const u=setTimeout(()=>r.abort(),i);try{return t.type==="http"?await _(t,n,o,a,r.signal):await J(t,n,o,a,r.signal)}finally{clearTimeout(u),e.signal?.removeEventListener("abort",c)}}function S(t,n,o){return Number.isNaN(t)?n:Math.min(o,Math.max(n,t))}async function _(t,n,o,e,i){if(!t.url)throw new Error("MCP server url is missing.");const a={jsonrpc:"2.0",id:`tg-agent-${Date.now()}`,method:n,params:o??{}},r={"content-type":"application/json"};t.auth&&(r.authorization=t.auth.startsWith("Bearer ")?t.auth:`Bearer ${t.auth}`);const c=Date.now(),u=await fetch(t.url,{method:"POST",headers:r,body:JSON.stringify(a),signal:i}),s=await $(u,e,i),d=u.headers.get("content-type");let l=s.text;try{const f=JSON.parse(s.text);l=JSON.stringify(f,null,2)}catch{}return{ok:u.ok,output:l,durationMs:Date.now()-c,bytes:s.bytes,truncated:s.truncated,statusCode:u.status,statusText:u.statusText,contentType:d}}async function J(t,n,o,e,i){if(!t.command)throw new Error("MCP server command is missing.");const a={jsonrpc:"2.0",id:`tg-agent-${Date.now()}`,method:n,params:o??{}},r=Date.now(),c=D(t.command,t.args??[],{stdio:["pipe","pipe","pipe"]}),u=()=>{c.killed||c.kill("SIGTERM")};if(i.aborted)throw u(),new Error("MCP request aborted.");i.addEventListener("abort",u,{once:!0});let s="",d="",l=!1,f;const h=N.createInterface({input:c.stdout,terminal:!1});h.on("line",m=>{if(s.length+m.length+1>e){l=!0;return}if(s+=m+`
2
+ `,f===void 0)try{f=JSON.parse(m)}catch{}}),c.stderr?.on("data",m=>{const g=m.toString();if(d.length+g.length>e){l=!0;return}d+=g});const p=`${JSON.stringify(a)}
3
+ `;c.stdin?.write(p),c.stdin?.end();const T=await new Promise(m=>{c.on("exit",(g,k)=>m({code:g,signal:k}))});if(h.close(),i.removeEventListener("abort",u),i.aborted)throw new Error("MCP request aborted.");if(!f&&s.trim())try{f=JSON.parse(s.trim())}catch{}if(T.code!==0&&!f){const m=d.trim()||"stdio process failed";throw new Error(m)}if(!f){const m=d.trim();throw m?new Error(m):new Error("MCP stdio returned no JSON.")}const b=JSON.stringify(f,null,2);return{ok:!0,output:b,durationMs:Date.now()-r,bytes:Math.min(b.length,e),truncated:l}}async function $(t,n,o){const e=t.body?.getReader?.();if(!e){const l=await t.text(),f=Math.min(l.length,n);return{text:l.slice(0,n),bytes:f,truncated:l.length>n}}const i=new TextDecoder("utf-8"),a=[];let r=0,c=!1;for(;;){if(o.aborted){try{await e.cancel()}catch{}throw new Error("MCP request aborted.")}const{done:l,value:f}=await e.read();if(l)break;if(!f)continue;const h=r+f.byteLength;if(h>n){const p=Math.max(0,n-r);p>0&&(a.push(f.slice(0,p)),r+=p),c=!0;try{await e.cancel()}catch{}break}a.push(f),r=h}const u=new Uint8Array(r);let s=0;for(const l of a)u.set(l,s),s+=l.byteLength;return{text:i.decode(u),bytes:r,truncated:c}}function x(t){const n=[];let o=null,e={};const i=()=>{if(!o)return;const a=e.type??(e.url?"http":e.command?"stdio":void 0);if(!a)return;const r=a==="http"||a==="stdio"?a:void 0;r&&(r==="http"&&!e.url||r==="stdio"&&!e.command||n.push({name:o,type:r,url:e.url,command:e.command,args:e.args,auth:e.auth}))};for(const a of t.split(/\r?\n/)){const r=q(a).trim();if(!r)continue;if(r.startsWith("[")&&r.endsWith("]")){i();const d=r.slice(1,-1).trim();if(!d.startsWith("mcp_servers.")){o=null,e={};continue}let l=d.slice(12).trim();if(l=w(l),!l){o=null,e={};continue}o=l,e={};continue}if(!o)continue;const c=r.indexOf("=");if(c===-1)continue;const u=r.slice(0,c).trim(),s=r.slice(c+1).trim();if(u)switch(u){case"type":e.type=y(s).toLowerCase();break;case"url":e.url=y(s);break;case"command":e.command=y(s);break;case"args":e.args=A(s);break;case"auth":case"authorization":case"token":e.auth=y(s);break;default:break}}return i(),n}function w(t){return t.startsWith('"')&&t.endsWith('"')||t.startsWith("'")&&t.endsWith("'")?t.slice(1,-1):t}function y(t){const n=t.trim();return n?n.startsWith('"')&&n.endsWith('"')?n.slice(1,-1).replace(/\\\\/g,"\\").replace(/\\"/g,'"'):n.startsWith("'")&&n.endsWith("'")?n.slice(1,-1):n:""}function A(t){const n=t.trim();if(!n.startsWith("[")||!n.endsWith("]"))return[];const o=n.slice(1,-1).trim();if(!o)return[];const e=[];let i="",a=!1,r="";for(let u=0;u<o.length;u+=1){const s=o[u];if(!a&&(s==='"'||s==="'")){a=!0,r=s;continue}if(a&&s===r){a=!1;continue}if(!a&&s===","){const d=i.trim();d&&e.push(w(d)),i="";continue}i+=s}const c=i.trim();return c&&e.push(w(c)),e}function q(t){let n=!1,o="";for(let e=0;e<t.length;e+=1){const i=t[e];if(!n&&(i==='"'||i==="'")){n=!0,o=i;continue}if(n&&i===o){n=!1;continue}if(!n&&(i==="#"||i===";"))return t.slice(0,e)}return t}export{O as callMcpServer,B as formatMcpTarget,M as getMcpConfigPath,Q as loadMcpServers,U as loadMcpServersSync,G as probeMcpServer};