@tomflow/chatloop-tools 0.4.11 → 0.4.13
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/lib/adapters/mcp-child.mjs +10 -5
- package/lib/control.mjs +5 -0
- package/lib/gateway.mjs +4 -3
- package/lib/supervisor.mjs +1 -1
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@ import {createRequire} from 'node:module';
|
|
|
3
3
|
import {dirname,join} from 'node:path';
|
|
4
4
|
import {mkdirSync,readFileSync} from 'node:fs';
|
|
5
5
|
import {randomUUID} from 'node:crypto';
|
|
6
|
-
import {paths} from '../control.mjs';
|
|
6
|
+
import {paths,toolCallDeadlineMs} from '../control.mjs';
|
|
7
7
|
|
|
8
8
|
const require=createRequire(import.meta.url);
|
|
9
9
|
const catalog=JSON.parse(readFileSync(new URL('../desktop-commander-tools.json',import.meta.url),'utf8'));
|
|
@@ -11,7 +11,10 @@ const fingerprint=tools=>JSON.stringify(tools.map(({name,inputSchema})=>({name,i
|
|
|
11
11
|
const expected=fingerprint(catalog);
|
|
12
12
|
const err=code=>({code});
|
|
13
13
|
const result=(tool,before,after,outcome,error=null,pid=null,evidence=null)=>({toolId:tool.id,enabled:tool.enabled,before,after,outcome,pid,evidence,error});
|
|
14
|
+
// Cold Desktop Commander initialization can exceed 9s on a busy macOS host.
|
|
15
|
+
// Startup has its own budget; ordinary tool calls keep the stricter deadline.
|
|
14
16
|
const timeout=ctx=>Math.max(0,Math.min(ctx.remaining(),9000));
|
|
17
|
+
export const startupHandshakeBudget=ctx=>Math.max(0,Math.min(ctx.remaining(),24000));
|
|
15
18
|
function entryFor(tool){
|
|
16
19
|
if(tool.id!=='desktop-commander')throw Object.assign(Error('Unknown MCP child'),{code:'UNKNOWN_TOOL'});
|
|
17
20
|
const pkg=require.resolve('@wonderwhy-er/desktop-commander/package.json');
|
|
@@ -69,7 +72,7 @@ export function createMcpChildAdapter({records,log}){
|
|
|
69
72
|
if(health.after==='RUNNING')return result(tool,before,'RUNNING','ALREADY_RUNNING',null,old.pid,'MCP_TOOLS_LIST');
|
|
70
73
|
return result(tool,before,'UNKNOWN','IDENTITY_UNCERTAIN',health.error,old.pid);
|
|
71
74
|
}
|
|
72
|
-
if(
|
|
75
|
+
if(startupHandshakeBudget(ctx)<100)return result(tool,before,before,'DEADLINE_EXPIRED',err('TIMEOUT'));
|
|
73
76
|
const home=join(paths().dir,'tools',tool.id,'home');
|
|
74
77
|
mkdirSync(home,{recursive:true,mode:0o700});
|
|
75
78
|
const env={HOME:home,PATH:process.env.PATH||'/usr/bin:/bin',LANG:process.env.LANG||'en_US.UTF-8',
|
|
@@ -80,10 +83,10 @@ export function createMcpChildAdapter({records,log}){
|
|
|
80
83
|
const rec=setup(tool,child);
|
|
81
84
|
try{
|
|
82
85
|
const info=await request(rec,'initialize',{protocolVersion:'2025-03-26',
|
|
83
|
-
capabilities:{},clientInfo:{name:'chat-loop-local-tools',version:'0.4.4'}},
|
|
86
|
+
capabilities:{},clientInfo:{name:'chat-loop-local-tools',version:'0.4.4'}},startupHandshakeBudget(ctx));
|
|
84
87
|
if(!info?.protocolVersion)throw Object.assign(Error('Handshake failed'),{code:'MCP_HANDSHAKE_FAILED'});
|
|
85
88
|
child.stdin.write(JSON.stringify({jsonrpc:'2.0',method:'notifications/initialized'})+'\n');
|
|
86
|
-
const listed=await request(rec,'tools/list',{},
|
|
89
|
+
const listed=await request(rec,'tools/list',{},startupHandshakeBudget(ctx));
|
|
87
90
|
if(!listed||fingerprint(listed.tools||[])!==expected)
|
|
88
91
|
throw Object.assign(Error('Tool schema changed'),{code:'SCHEMA_MISMATCH'});
|
|
89
92
|
rec.state='RUNNING';log('tool-start',{requestId:ctx.requestId,toolId:tool.id,pid:rec.pid});
|
|
@@ -128,7 +131,9 @@ export function createMcpChildAdapter({records,log}){
|
|
|
128
131
|
if(!catalog.some(t=>t.name===name))throw Object.assign(Error('No such tool'),{code:'UNKNOWN_TOOL'});
|
|
129
132
|
rec.active++;
|
|
130
133
|
try{
|
|
131
|
-
|
|
134
|
+
// Leave time for IPC and Gateway acknowledgements; never exceed the caller's deadline.
|
|
135
|
+
const ms=Math.max(0,Math.min(ctx.remaining(),toolCallDeadlineMs(args)-2000));
|
|
136
|
+
return await request(rec,'tools/call',{name,arguments:args},ms);
|
|
132
137
|
}catch(e){if(e.code==='TIMEOUT')rec.state='UNKNOWN';throw e;}
|
|
133
138
|
finally{rec.active--;}
|
|
134
139
|
}
|
package/lib/control.mjs
CHANGED
|
@@ -5,6 +5,11 @@ import {join} from 'node:path';
|
|
|
5
5
|
|
|
6
6
|
export const CONTRACT = 'chat-loop.local-control.v1';
|
|
7
7
|
export const VERSION = '0.4.7';
|
|
8
|
+
// Bounded end-to-end tool calls: long-running Desktop Commander operations may wait up to timeout_ms.
|
|
9
|
+
export const toolCallDeadlineMs = args => {
|
|
10
|
+
const requested = Number.isSafeInteger(args?.timeout_ms) && args.timeout_ms > 0 ? args.timeout_ms : 0;
|
|
11
|
+
return Math.min(42000, Math.max(12000, requested + 8000));
|
|
12
|
+
};
|
|
8
13
|
export const ACTIONS = new Set(['start','stop','restart','status']);
|
|
9
14
|
export const directory = () => process.env.CHAT_LOOP_LOCAL_TOOLS_STATE_DIR || join(homedir(), '.chat-loop', 'local-tools');
|
|
10
15
|
export const paths = () => ({dir: directory(), socket: join(directory(), 'control.sock'), lock: join(directory(), 'manager.lock'), log: join(directory(), 'events.jsonl')});
|
package/lib/gateway.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Stdio MCP entrypoint: protocol framing and routing only; never owns child processes.
|
|
2
2
|
import {randomUUID} from 'node:crypto';
|
|
3
3
|
import {readFileSync} from 'node:fs';
|
|
4
|
-
import {CONTRACT,registry,talk} from './control.mjs';
|
|
4
|
+
import {CONTRACT,registry,talk,toolCallDeadlineMs} from './control.mjs';
|
|
5
5
|
import {extensionTools,forwardExtensionAction} from './extension-proxy.mjs';
|
|
6
6
|
|
|
7
7
|
const desktopTools=JSON.parse(readFileSync(new URL('./desktop-commander-tools.json',import.meta.url),'utf8'));
|
|
@@ -46,7 +46,7 @@ export function runGateway(){
|
|
|
46
46
|
return reject(m.id,-32602,'UNSUPPORTED_MCP_VERSION');
|
|
47
47
|
connected=true;
|
|
48
48
|
return reply(m.id,{protocolVersion:m.params.protocolVersion,
|
|
49
|
-
capabilities:{tools:{}},serverInfo:{name:'chat-loop-local-tools',version:'0.4.
|
|
49
|
+
capabilities:{tools:{}},serverInfo:{name:'chat-loop-local-tools',version:'0.4.13'}});
|
|
50
50
|
}
|
|
51
51
|
// A modern MCP client probes discovery before falling back to legacy initialize.
|
|
52
52
|
// We only implement the legacy handshake era, so report unsupported method.
|
|
@@ -63,8 +63,9 @@ export function runGateway(){
|
|
|
63
63
|
try{
|
|
64
64
|
if(routed.toolId==='chat-loop-extension')
|
|
65
65
|
return reply(m.id,await forwardExtensionAction(routed.name,args));
|
|
66
|
+
const deadlineMs=toolCallDeadlineMs(args);
|
|
66
67
|
const res=await talk({contract:CONTRACT,requestId:randomUUID(),action:'call',
|
|
67
|
-
scope:'tool',toolId:routed.toolId,name:routed.name,args,deadlineMs
|
|
68
|
+
scope:'tool',toolId:routed.toolId,name:routed.name,args,deadlineMs},deadlineMs+2000);
|
|
68
69
|
if(res.status!=='OK')return reply(m.id,failure(res.error?.code||'TOOL_UNAVAILABLE'));
|
|
69
70
|
return reply(m.id,res.result);
|
|
70
71
|
}catch(e){
|
package/lib/supervisor.mjs
CHANGED
|
@@ -40,7 +40,7 @@ async function executeCall(req){
|
|
|
40
40
|
if(typeof req.requestId!=='string'||req.requestId.length<6||req.requestId.length>128||
|
|
41
41
|
req.scope!=='tool'||typeof req.toolId!=='string'||typeof req.name!=='string'||
|
|
42
42
|
!req.args||typeof req.args!=='object'||Array.isArray(req.args)||
|
|
43
|
-
!Number.isInteger(req.deadlineMs)||req.deadlineMs<1000||req.deadlineMs>
|
|
43
|
+
!Number.isInteger(req.deadlineMs)||req.deadlineMs<1000||req.deadlineMs>45000)return invalid();
|
|
44
44
|
const tool=catalog.find(t=>t.id===req.toolId);
|
|
45
45
|
if(!tool)return bad(req,'UNKNOWN_TOOL');
|
|
46
46
|
if(!tool.enabled)return bad(req,'DISABLED_TOOL');
|