UNPKG

grix-connector

Version:

Connect local AI coding agents (Claude, Codex, Gemini, Qwen, DeepSeek, Cursor, OpenCode, Pi, OpenHuman, Reasonix) to the Grix scheduling platform. Also serves as an OpenClaw plugin for Grix channel transport.

11 lines (10 loc) 4.48 kB
#!/usr/bin/env node import R from"node:http";import S from"node:net";import c from"node:process";import{Server as v}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as A}from"@modelcontextprotocol/sdk/server/stdio.js";import{CallToolRequestSchema as I,ListToolsRequestSchema as x}from"@modelcontextprotocol/sdk/types.js";import{toolCallToInvoke as P,mapToolAlias as L,EXPOSED_TOOLS as k,TOOL_MAP as $,PHASE2_TOOL_NAMES as C}from"../../core/mcp/tools.js";function O(t,e){const n=c.argv.slice(2);for(let r=0;r<n.length;r++)if(n[r]===`--${t}`&&n[r+1])return n[r+1];return c.env[e]||void 0}const w=O("handle-url","GRIX_CONNECTOR_INTERNAL_API")??"";w||(c.stderr.write(`FATAL: --handle-url <url> required (or set GRIX_CONNECTOR_INTERNAL_API) `),c.exit(1));const M=parseInt(O("notify-port","GRIX_MCP_NOTIFY_PORT")??"0",10),u=[1e3,2e3,4e3];function Y(t){if(t instanceof Error){const e=t.message;if(e.includes("ECONNREFUSED")||e.includes("ECONNRESET")||e.includes("ETIMEDOUT")||e.includes("socket hang up"))return!0;if(e.includes("invoke timeout"))return!1}return!1}async function y(t,e,n=15e3,r=0){try{return await D(t,e,n)}catch(o){if(r>0&&Y(o)){const i=u[u.length-r]??u[u.length-1];return c.stderr.write(`[grix-stdio] invokeApi retry (${u.length-r+1}/${u.length}) action=${t} after ${i}ms: ${o instanceof Error?o.message:o} `),await new Promise(s=>setTimeout(s,i)),y(t,e,n,r-1)}throw o}}function D(t,e,n){const r=new URL(w),o=new URL("/api/invoke",r),i=r.searchParams.get("token")??"";i&&o.searchParams.set("token",i);const s=JSON.stringify({action:t,params:e,timeout_ms:n});return new Promise((l,a)=>{const T=setTimeout(()=>a(new Error("invoke timeout")),n+5e3),h=R.request(o,{method:"POST",headers:{"content-type":"application/json","content-length":Buffer.byteLength(s),...i?{authorization:`Bearer ${i}`}:{}}},m=>{const N=[];m.on("data",f=>N.push(f)),m.on("end",()=>{clearTimeout(T);const f=Buffer.concat(N).toString("utf8");try{const g=JSON.parse(f);g.ok?l(g.data??null):a(new Error(g.error??"invoke failed"))}catch{a(new Error(`invalid response: ${f.slice(0,200)}`))}})});h.on("error",m=>{clearTimeout(T),a(m)}),h.write(s),h.end()})}const p=new v({name:"grix",version:"1.0.0"},{capabilities:{experimental:{"claude/channel":{},"claude/channel/permission":{}},tools:{}},instructions:['Messages arrive as <channel source="grix-claude" chat_id="..." event_id="..." message_id="..." user_id="...">text</channel>.',"IMPORTANT: You MUST use the reply tool to send any response to the user.","Your plain text output is NOT delivered to the chat \u2014 only the reply tool delivers messages.","Always call the reply tool with chat_id, event_id, and your response text.","If you intentionally do not want to send a visible reply, you must call the complete tool with event_id and a final status."].join(" ")});p.setRequestHandler(x,async()=>({tools:k.map(t=>({name:t.name,description:t.description,inputSchema:t.inputSchema}))})),p.setRequestHandler(I,async t=>{const e=String(t.params.name??""),n=t.params.arguments??{},r=L(e,n),o=r.name,i=r.args;if(!$.has(e))return{content:[{type:"text",text:`Unknown tool: ${e}`}],isError:!0};try{if(C.has(o)){const a=await y("event_tool_call",{tool_name:o,arguments:i},3e4,0);return{content:[{type:"text",text:typeof a=="string"?a:JSON.stringify(a,null,2)}]}}const s=P(o,i),l=await y(s.action,s.params);return{content:[{type:"text",text:JSON.stringify(l,null,2)}]}}catch(s){const l=s instanceof Error?s.message:String(s);return{content:[{type:"text",text:JSON.stringify({error:l})}],isError:!0}}});let _=!1;const d=[],F=100;async function E(t,e){if(!_){d.push({method:t,params:e}),d.length>F&&d.shift();return}try{await p.notification({method:t,params:e})}catch(n){c.stderr.write(`DISPATCH_ERROR:${t}:${n} `)}}async function U(){for(;_&&d.length>0;){const t=d.shift();await E(t.method,t.params)}}function b(t){if(t<=0)return;const e=S.createServer(n=>{let r="";n.setEncoding("utf8"),n.on("data",o=>{r+=o;const i=r.split(` `);r=i.pop()??"";for(const s of i){const l=s.trim();if(l)try{const a=JSON.parse(l);a.method&&E(a.method,a.params??{}).catch(()=>{})}catch{}}}),n.on("error",()=>{})});e.listen(t,"127.0.0.1",()=>{c.stderr.write(`NOTIFY_READY:${t} `)}),e.on("error",n=>{c.stderr.write(`NOTIFY_ERROR: ${n.message} `)})}async function q(){b(M);const t=new A;await p.connect(t),_=!0,c.stderr.write(`[MAIN] MCP connected, ready to dispatch notifications `),await U()}q().catch(t=>{c.stderr.write(`FATAL: ${t} `),c.exit(1)});