infra-kit 0.5.6 → 0.7.0
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/chunk-63VNUKUS.js +8 -0
- package/dist/chunk-63VNUKUS.js.map +7 -0
- package/dist/chunk-7YE5QKKB.js +444 -0
- package/dist/chunk-7YE5QKKB.js.map +7 -0
- package/dist/chunk-GYPVEWUV.js +2 -0
- package/dist/chunk-GYPVEWUV.js.map +7 -0
- package/dist/chunk-KXQQUBGV.js +10 -0
- package/dist/{chunk-744Y674Y.js.map → chunk-KXQQUBGV.js.map} +4 -4
- package/dist/chunk-MFLYORUJ.js +2 -0
- package/dist/chunk-MFLYORUJ.js.map +7 -0
- package/dist/chunk-NFIFWIO6.js +4 -0
- package/dist/chunk-NFIFWIO6.js.map +7 -0
- package/dist/chunk-QS4JZKF2.js +2 -0
- package/dist/{chunk-YYEUJYC7.js.map → chunk-QS4JZKF2.js.map} +1 -1
- package/dist/chunk-UI2MUEVR.js +2 -0
- package/dist/chunk-UI2MUEVR.js.map +7 -0
- package/dist/{chunk-QUXDC3A3.js → chunk-YKCY657C.js} +4 -4
- package/dist/cli.js +11 -10
- package/dist/cli.js.map +3 -3
- package/dist/dev-server.js +22 -22
- package/dist/dev-server.js.map +3 -3
- package/dist/{dev-wizard-run-DWDA5R3A.js → dev-wizard-run-MCUYV6IN.js} +2 -2
- package/dist/mcp-proxy.js +9 -9
- package/dist/mcp-proxy.js.map +3 -3
- package/dist/mcp.js +2 -2
- package/dist/mcp.js.map +4 -4
- package/dist/update-check.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-744Y674Y.js +0 -10
- package/dist/chunk-LCNORKDV.js +0 -8
- package/dist/chunk-LCNORKDV.js.map +0 -7
- package/dist/chunk-PQG63EIV.js +0 -4
- package/dist/chunk-PQG63EIV.js.map +0 -7
- package/dist/chunk-XJOY7J3G.js +0 -2
- package/dist/chunk-XJOY7J3G.js.map +0 -7
- package/dist/chunk-XN7VHD7P.js +0 -445
- package/dist/chunk-XN7VHD7P.js.map +0 -7
- package/dist/chunk-Y5TLBGZT.js +0 -2
- package/dist/chunk-Y5TLBGZT.js.map +0 -7
- package/dist/chunk-YYEUJYC7.js +0 -2
- /package/dist/{chunk-QUXDC3A3.js.map → chunk-YKCY657C.js.map} +0 -0
- /package/dist/{dev-wizard-run-DWDA5R3A.js.map → dev-wizard-run-MCUYV6IN.js.map} +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{f as C,i as f,k as g}from"./chunk-
|
|
2
|
-
//# sourceMappingURL=dev-wizard-run-
|
|
1
|
+
import{f as C,i as f,k as g}from"./chunk-YKCY657C.js";import{K as u,R as y,U as k,V as x,X as v,fa as w}from"./chunk-63VNUKUS.js";import"./chunk-MFAFQVTW.js";import{f as b}from"./chunk-NFIFWIO6.js";import"./chunk-QWVLAZ6N.js";import O from"@inquirer/checkbox";import _ from"@inquirer/confirm";import j,{Separator as I}from"@inquirer/select";import{loadDev as K}from"@slip-stream-kit/config/internal";import U from"node:path";import m from"node:process";var W=e=>e.split("/")[0],A=e=>new Map(e.apps.map(r=>[r.name,r])),D=e=>e.localCapable&&e.cloudCapable,N=(e,r,t)=>D(r)?t[`${e} ${r.path}`]??(r.cloudCapable?"cloud":"local"):r.localCapable?"local":"cloud",$=(e,r,t)=>{let a=new Set,o=new Map;for(let n of e){let s=r.get(W(n));for(let c of s?.backends??[]){o.set(c.packageName,c.ownerApp);for(let l of c.routes)N(n,l,t)==="local"&&a.add(c.packageName)}}return{localPkgs:a,ownerByPkg:o}},B=(e,r)=>{let t={},a=!1;for(let o of e?.backends??[]){let n=r.has(o.packageName);for(let s of o.routes){let c=s.localCapable&&n?"local":"cloud";t[s.path]=c,c==="cloud"&&(a=!0)}}return{overrides:t,anyCloud:a}},z=(e,r)=>{let t=A(r),a=e.targets.filter(p=>p.endsWith("/ui")),o=e.targets.filter(p=>p.endsWith("/api")),{localPkgs:n,ownerByPkg:s}=$(a,t,e.sources),c={},l=!1;for(let p of a){let{overrides:d,anyCloud:i}=B(t.get(W(p)),n);i&&(l=!0),c[p]=Object.keys(d).length>0?{proxy:d}:{}}for(let p of o)c[p]??={};for(let p of n){let d=s.get(p);d!=null&&(c[`${d}/api`]??={})}return{presetDef:{apps:c,cmux:e.cmux},anyCloudRoute:l,targetKeys:Object.keys(c).sort()}},M=(e,r)=>{let t=[`--target=${e.targetKeys.join(",")}`];return r.watch&&t.push("--watch"),r.cmux&&t.push("--cmux"),{flags:t.join(" ")}};var h={output:m.stderr,clearPromptOnDone:!0},q={select:e=>g(r=>j({message:e.message,choices:e.choices,default:e.default},r),h),checkbox:e=>g(r=>O({message:e.message,choices:e.choices},r),h),confirm:e=>g(r=>_({message:e.message,default:e.default},r),h)},E=(e,r)=>{let t=new Map;for(let[a,o]of Object.entries(e)){let n={path:a,localCapable:o.from.includes("local"),cloudCapable:o.from.includes("cloud"),default:o.default},s=t.get(o.packageName);s?(s.routes.push(n),s.localCapable=s.localCapable||n.localCapable,s.cloudCapable=s.cloudCapable||n.cloudCapable):t.set(o.packageName,{packageName:o.packageName,routes:[n],localCapable:n.localCapable,cloudCapable:n.cloudCapable,ownerApp:r.get(o.packageName)})}return[...t.values()]},T=async(e,r,t)=>{try{let a=(await K(e))?.proxy?.routes;return a?E(a,t):[]}catch(a){return u.warn(`\u26A0\uFE0F Skipping ${r}: its dev config failed to load \u2014 ${a instanceof Error?a.message:String(a)}`),[]}},V=async e=>{let r=x(e),t=v(e),a=await y(),o=new Map(r.map(i=>[i.packageName,i.name])),n=new Map(r.map(i=>[i.name,i.packageName])),s=new Set(t.map(i=>i.name)),c=new Set(r.map(i=>i.name)),l=[...new Set([...c,...s])].sort(),p=await Promise.all(l.map(async i=>{let P=s.has(i);return{name:i,hasApi:c.has(i),hasUi:P,apiPackage:n.get(i),backends:P?await T(U.join(e,"apps",i,"ui"),i,o):[]}})),d=await C();return{apps:p,presets:Object.keys(a.devServersPresets??{}),environments:Object.keys(d?.envs??{}).sort()}},F=e=>{let r=[];for(let t of e.apps){if(t.hasUi){let a=t.backends.flatMap(n=>n.routes.map(s=>s.path)),o=a.length>0?`frontend \u2014 proxies ${[...a].sort().join(", ")}`:"frontend";r.push({name:`${t.name}/ui`,value:`${t.name}/ui`,description:o})}t.hasApi&&!t.hasUi&&r.push({name:`${t.name}/api`,value:`${t.name}/api`,description:"backend"})}return r},L=e=>{let r={api:e.apps.filter(o=>o.hasApi).map(o=>o.name),ui:e.apps.filter(o=>o.hasUi).map(o=>o.name)},t={},a=new Map;for(let o of e.apps){o.apiPackage!=null&&(t[o.name]=o.apiPackage);for(let n of o.backends)for(let s of n.routes)a.set(`${o.name} ${s.path}`,n.packageName)}return{discovered:r,apiPkgByApp:t,routePkg:(o,n)=>a.get(`${o} ${n}`)}},X=(e,r)=>w({__wizard__:e},L(r)).map(t=>t.message),Z="INFRA_KIT_DEV_WIZARD_CMUX",H=()=>m.env[Z]==="1",G=async(e,r,t)=>{let a=new Map(r.apps.map(n=>[n.name,n])),o={};for(let n of t){let s=a.get(n.split("/")[0]);for(let c of s?.backends??[])for(let l of c.routes){if(!l.localCapable||!l.cloudCapable)continue;let p=await e.select({message:`\u{1F500} ${n} ${l.path} \u2192 local or cloud?`,choices:[{name:"local (this machine)",value:"local"},{name:"cloud",value:"cloud"}],default:l.default??"local"});o[`${n} ${l.path}`]=p}}return o},J=(e,r,t)=>e.apps.find(a=>a.name===r)?.backends.flatMap(a=>a.routes).find(a=>a.path===t),Q=(e,r)=>{let t=[];for(let[a,o]of Object.entries(e.presetDef.apps??{})){if(!a.endsWith("/ui")||!o.proxy)continue;let n=a.split("/")[0];for(let[s,c]of Object.entries(o.proxy)){let l=J(r,n,s),p=l&&l.localCapable&&l.cloudCapable?"":" (only option)";t.push(` ${a} ${s} \u2192 ${c}${p}`)}}if(t.length!==0){u.info("Resolved routes:");for(let a of[...t].sort())u.info(a)}},R=async(e,r)=>{if(r.apps.length===0)return u.warn("No apps discovered to run."),null;let t=await e.checkbox({message:"\u{1F4E6} Which packages?",choices:F(r)});if(t.length===0)return u.warn("No packages selected."),null;let a=t.filter(i=>i.endsWith("/ui")),o=await G(e,r,a),n=await e.confirm({message:"\u{1F440} Rebuild & restart on save (watch)?",default:!1}),s=H()?await e.confirm({message:"\u{1F9E9} Run each app in its own cmux pane?",default:!1}):!1,c={targets:t,sources:o,watch:n,cmux:s},l=z(c,r);if(l.anyCloudRoute){if(r.environments.length===0)return u.warn("\u2601\uFE0F This selection routes to a cloud environment, but no environment token is loaded."),u.warn(" Run `infra-kit env-load` to add one, then re-run the wizard."),null;c.env=await e.select({message:"\u2601\uFE0F Point cloud routes at which environment?",choices:r.environments.map(i=>({name:i,value:i}))}),m.env[b]=c.env}let p=X(l.presetDef,r);if(p.length>0){u.warn("\u26A0\uFE0F Proxy audit found issues with this selection:");for(let i of p)u.warn(` \u2022 ${i}`);return null}Q(l,r),Y(l,c);let d=l.targetKeys.filter(i=>i.endsWith("/api")).map(i=>i.split("/")[0]);return s&&d.length>0?{include:d,presetDef:l.presetDef,watch:n,cmux:!0}:(s&&u.info("\u2139\uFE0F cmux needs at least one local backend (panes are backend-only) \u2014 running in-process instead."),{presetDef:l.presetDef,watch:n,cmux:!1})},Y=(e,r)=>{f.setInteractive();let t=M(e,r);f.addOption(t.flags,!0),f.print(),u.info("\u2139\uFE0F Prefer a short name? Save this selection as a devPreset and run `infra-kit dev <name>`.")},ee=async(e,r)=>{let t=await e.confirm({message:"\u{1F440} Rebuild & restart on save (watch)?",default:!1});return f.setInteractive(),f.addOption(r,!0),t&&f.addOption("--watch",!0),f.print(),{preset:r,watch:t,cmux:!1}},S=" manual",re=async(e,r)=>{if(r.presets.length===0)return R(e,r);let t=await e.select({message:"\u{1F680} Start from a preset, or configure manually?",choices:[...r.presets.map(a=>({name:a,value:a})),new I(" "),{name:"Manual (custom)\u2026",value:S}]});return t===S?R(e,r):ee(e,t)},be=async(e=q,r=k(m.cwd()))=>re(e,await V(r));export{S as MANUAL_CHOICE,Z as WIZARD_CMUX_VAR,X as auditManualPlan,q as defaultPrompts,V as gatherWizardModel,T as loadBackends,be as runDevWizard,re as runWizardFlow};
|
|
2
|
+
//# sourceMappingURL=dev-wizard-run-MCUYV6IN.js.map
|
package/dist/mcp-proxy.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{a as
|
|
3
|
-
`)[0]?.trim()??"";return a.length>0?a:A(e,r)}catch{return A(e,r)}},
|
|
4
|
-
`,384),!0}catch{return!1}};import we from"node:path";import L from"node:process";var
|
|
5
|
-
`);for(;a!==-1;){let
|
|
6
|
-
`)}})};import{spawn as
|
|
7
|
-
`)},q=(t,i,
|
|
8
|
-
`))S.length>0&&f(`${r}: ${S}`)}),
|
|
9
|
-
`)},
|
|
10
|
-
`)},I=
|
|
2
|
+
import{u as G}from"./chunk-UI2MUEVR.js";import{a as M}from"./chunk-MFAFQVTW.js";import"./chunk-GYPVEWUV.js";import{a as W,d as H,n as T,o as E,s as K}from"./chunk-NFIFWIO6.js";import m from"node:process";var ve=/^[a-z][a-z0-9-]*$/,ye=/^[A-Z_]\w*$/i,Pe=(e,n,r)=>n==="--name"?e.name!==null?"--name given twice":ve.test(r)?(e.name=r,null):`--name "${r}" must be lowercase letters, digits and hyphens`:n==="--env"||n==="--unset"?ye.test(r)?M.has(r)?`${n} "${r}" is process plumbing, not a credential`:(n==="--env"?e.env.push(r):e.unset.push(r),null):`${n} "${r}" is not an environment-variable name`:`unknown flag ${n}`,Se=(e,n)=>{let r=0;for(;r<e.length;){let o=e[r];if(o==="--")return{separator:r};if(!o.startsWith("--"))return{problem:`missing "--" before the upstream command (saw "${o}")`};let a=e[r+1];if(a===void 0||a.startsWith("--"))return{problem:`${o} needs a value`};let p=Pe(n,o,a);if(p!==null)return{problem:p};r+=2}return{problem:'missing "--" before the upstream command'}},Q=e=>{let n={name:null,env:[],unset:[]},r=Se(e,n);if("problem"in r)return{ok:!1,problem:r.problem};if(n.name===null)return{ok:!1,problem:"missing --name"};if(n.env.length===0)return{ok:!1,problem:"missing --env (at least one variable to read)"};let o=e[r.separator+1];return o===void 0||o.length===0?{ok:!1,problem:'missing upstream command after "--"'}:{ok:!0,spec:{name:n.name,command:o,args:e.slice(r.separator+2),env:n.env,unset:n.unset}}};import{spawnSync as xe}from"node:child_process";import C from"node:fs";import x from"node:path";import O from"node:process";var Y=e=>x.join(E(),"mcp-proxy",e),be=e=>e.replaceAll(/[^\w.-]/g,"_").slice(0,64),Z=(e,n)=>x.join(n,`profile-${be(e)}.json`),A=(e,n)=>{let r=x.isAbsolute(e)?[e]:(n??O.env.PATH??"").split(x.delimiter).map(o=>x.join(o,e));for(let o of r)try{let a=C.statSync(o);return`stat-${a.size}-${Math.trunc(a.mtimeMs)}`}catch{}return"unknown"},X=(e,n=["--version"],r)=>{try{let o=xe(e,[...n],{encoding:"utf8",timeout:5e3,env:r==null?O.env:{...O.env,PATH:r}});if(o.status!==0)return A(e,r);let a=`${o.stdout}`.trim().split(`
|
|
3
|
+
`)[0]?.trim()??"";return a.length>0?a:A(e,r)}catch{return A(e,r)}},ee=(e,n)=>{try{let r=JSON.parse(C.readFileSync(Z(e,n),"utf8"));return r.version!==e||!Array.isArray(r.tools)?null:{version:e,protocolVersion:typeof r.protocolVersion=="string"?r.protocolVersion:"",capabilities:r.capabilities??{},tools:r.tools}}catch{return null}},re=(e,n)=>{try{return C.mkdirSync(n,{recursive:!0}),K(Z(e.version,n),`${JSON.stringify(e)}
|
|
4
|
+
`,384),!0}catch{return!1}};import we from"node:path";import L from"node:process";var Ee="no-session",Me="IK_MCP_ENV_FILE",Re=()=>{let e=L.env[H];return e!=null&&e.length>0?e:Ee},R=()=>{let e=L.env[Me];return e!=null&&e.length>0?e:we.join(E(),Re(),W)},$e=(e,n)=>{let r=e[n];if(r!=null&&r.length>0)return r;let o=L.env[n];return o!=null&&o.length>0?o:""},ne=(e,n=R())=>{let r=T(n),o={};for(let a of e){let p=$e(r,a);if(p.length===0)return null;o[a]=p}return o},te=(e,n=R())=>{let r=new Set(e);return Object.keys(T(n)).filter(o=>!r.has(o))};var $=(e,n)=>{let r="";e.setEncoding("utf8"),e.on("data",o=>{r+=o;let a=r.indexOf(`
|
|
5
|
+
`);for(;a!==-1;){let p=r.slice(0,a);r=r.slice(a+1);let f=p.endsWith("\r")?p.slice(0,-1):p;f.trim().length>0&&n(f),a=r.indexOf(`
|
|
6
|
+
`)}})};import{spawn as ke}from"node:child_process";import Je from"node:os";import F from"node:path";import k from"node:process";var Ie=()=>{let e=k.env.GOBIN;if(e!=null&&e.length>0)return e;let n=k.env.GOPATH;return F.join(n!=null&&n.length>0?n:F.join(Je.homedir(),"go"),"bin")},Ve=15e3,_e=12e4,Ne=M,oe=(e,n={})=>{let r=e.command,o=e.args,a=n.readyTimeoutMs??Ve,p=n.requestTimeoutMs??_e,f=n.log??(()=>{}),ce=n.onServerMessage??(()=>{}),de=`${Ie()}${F.delimiter}${k.env.PATH??""}`,z={},d=null,y=null,_=0,ue=(t,i)=>{for(let s of t.pending.values())s.reject(new Error(i));t.pending.clear()},P=(t,i)=>{t.closed||(t.closed=!0,ue(t,`${e.name} upstream ${i}`),t.child.kill())},me=(t,i)=>{let s;try{s=JSON.parse(i)}catch{f(`ignoring non-JSON line from ${r}`);return}if(s.method!=null){if(s.id==null){ce(s);return}f(`refusing server-initiated request ${s.method}`),w(t,{jsonrpc:"2.0",id:s.id,error:{code:-32601,message:"client requests are not supported by this shim"}});return}if(s.id==null)return;let l=t.pending.get(Number(s.id));l&&(t.pending.delete(Number(s.id)),l.resolve(l.clientId===void 0?s:{...s,id:l.clientId}))},w=(t,i)=>{t.closed||t.child.stdin.write(`${JSON.stringify(i)}
|
|
7
|
+
`)},q=(t,i,s)=>{if(t.closed)return Promise.reject(new Error(`${e.name} upstream is closed`));let l=t.nextId;return t.nextId+=1,new Promise((c,S)=>{let B=setTimeout(()=>{t.pending.delete(l),S(new Error(`${r} did not answer ${i.method??"request"} within ${p}ms`))},p);t.pending.set(l,{clientId:s,resolve:N=>{clearTimeout(B),c(N)},reject:N=>{clearTimeout(B),S(N)}}),w(t,{...i,jsonrpc:"2.0",id:l})})},pe=async t=>{let i,s=new Promise((l,c)=>{i=setTimeout(()=>{c(new Error(`${r} did not answer initialize within ${a}ms`))},a)});try{return await Promise.race([t,s])}finally{i&&clearTimeout(i)}},fe=t=>{let i={...k.env,...n.extraEnv,PATH:de,...t};for(let s of e.unset)i[s]="";for(let s of n.stripFromChildEnv?.()??[])Ne.has(s)||delete i[s];return ke(r,[...o],{env:i,stdio:["pipe","pipe","pipe"]})},ge=async(t,i)=>{let s=fe(t),l={child:s,key:JSON.stringify(t),generation:i,pending:new Map,nextId:1,handshake:null,closed:!1};$(s.stdout,c=>{me(l,c)}),s.stderr.on("data",c=>{for(let S of c.toString().trimEnd().split(`
|
|
8
|
+
`))S.length>0&&f(`${r}: ${S}`)}),s.on("error",c=>{P(l,`failed to start: ${c.message}`)}),s.on("exit",c=>{P(l,`exited with code ${String(c)}`)});try{l.handshake=await pe(q(l,{method:"initialize",params:z}))}catch(c){throw P(l,"handshake failed"),c}return w(l,{jsonrpc:"2.0",method:"notifications/initialized"}),f(`serving ${e.name} (${r})`),l},he=async t=>{let i=JSON.stringify(t);if(d&&!d.closed&&d.key===i)return d;if(y&&y.key===i)return y.promise;_+=1;let s=_;d&&f(`${e.name}: a listed variable changed \u2014 replacing upstream`);let l=(async()=>{let c=await ge(t,s);if(s!==_)throw P(c,"superseded by a newer target"),new Error(`${e.name} upstream superseded by a newer target`);return d&&P(d,"replaced"),d=c,c})();y={key:i,generation:s,promise:l};try{return await l}finally{y?.generation===s&&(y=null)}};return{setInitializeParams:t=>{z=t},request:async(t,i)=>{let s=await he(i);return q(s,{method:t.method,params:t.params},t.id)},notifyIfActive:t=>!d||d.closed?!1:(w(d,{jsonrpc:"2.0",method:t.method,params:t.params}),!0),handshake:()=>d?.handshake??null,dispose:()=>{d&&P(d,"disposed"),d=null}}};var Te=Number(m.env.IK_MCP_POLL_MS??2e3),ie="2025-06-18",ae={tools:{listChanged:!0}},g=e=>{m.stderr.write(`ik-mcp: ${e}
|
|
9
|
+
`)},h=e=>{m.stdout.write(`${JSON.stringify(e)}
|
|
10
|
+
`)};G((e,n)=>{e.outcome==="failed"&&g(`${n} failed: ${e.link} -> ${e.target}`)});var I=Q(m.argv.slice(2)),j=I.ok?null:`${I.problem} \u2014 run \`ik setup\` to regenerate this .mcp.json entry`,u=I.ok?I.spec:null,v=u===null?null:oe(u,{log:g,onServerMessage:h,stripFromChildEnv:()=>te(u.env)}),D=()=>u===null?null:ne(u.env),se=null,J,V=()=>(se??=u===null?"unknown":X(u.command,["--version"],m.env.PATH),se),le=()=>u===null?null:Y(u.name),b=()=>{if(J===void 0){let e=le();J=e===null?null:ee(V(),e)}return J},Ae=e=>{let n=le();if(n===null)return;let r=b(),o={version:V(),protocolVersion:e.protocolVersion??r?.protocolVersion??ie,capabilities:e.capabilities??r?.capabilities??ae,tools:e.tools??r?.tools??[]};J=o,re(o,n)},Oe=e=>{h({jsonrpc:"2.0",id:e.id,result:{protocolVersion:b()?.protocolVersion??ie,capabilities:b()?.capabilities??ae,serverInfo:{name:u?.name??"ik-mcp",version:V()}}})},U=!1,Ce=()=>{let e=setInterval(()=>{U||D()===null||(U=!0,clearInterval(e),g("credentials appeared \u2014 announcing tools/list_changed"),h({jsonrpc:"2.0",method:"notifications/tools/list_changed"}))},Te);e.unref()},Le=async(e,n)=>{let r=await v.request(e,n),o=r.result?.tools;if(Array.isArray(o)){let a=v.handshake()?.result;Ae({tools:o,capabilities:a?.capabilities,protocolVersion:a?.protocolVersion})}h(r)},Fe=e=>{h({jsonrpc:"2.0",id:e.id,result:{tools:b()?.tools??[]}})},je=e=>{if(v?.setInitializeParams(e.params),Oe(e),j!==null){g(`degraded: ${j}`);return}if(g(`${u.name}: env file ${R()}; reads ${u.env.join(", ")}; upstream ${u.command} ${V()}; cache ${b()?"warm":"cold"}`),D()!==null){U=!0;return}g("no credentials yet \u2014 deferring"),Ce()},Ue=e=>{if(e.method==="tools/list"){Fe(e);return}e.id!=null&&h({jsonrpc:"2.0",id:e.id,error:{code:-32e3,message:j??`${u.name}: credentials are not loaded yet (${u.env.join(", ")}) \u2014 run 'ik env-load'`}})},De=async e=>{if(e.method==null)return;if(e.method==="initialize"){je(e);return}let n=D();if(n===null||v===null){Ue(e);return}if(e.id==null){e.method!=="notifications/initialized"&&v.notifyIfActive(e);return}if(e.method==="tools/list"){await Le(e,n);return}h(await v.request(e,n))},ze=e=>{let n;try{n=JSON.parse(e)}catch{g("ignoring non-JSON line from client");return}De(n).catch(r=>{let o=r instanceof Error?r.message:String(r);g(o),n.id!=null&&h({jsonrpc:"2.0",id:n.id,error:{code:-32e3,message:o}})})};m.on("exit",()=>{v?.dispose()});m.on("SIGTERM",()=>{m.exit(0)});m.on("SIGINT",()=>{m.exit(0)});$(m.stdin,ze);m.stdin.on("end",()=>{m.exit(0)});
|
|
11
11
|
//# sourceMappingURL=mcp-proxy.js.map
|
package/dist/mcp-proxy.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/entry/mcp-proxy.ts", "../src/lib/mcp-proxy/argv.ts", "../src/lib/mcp-proxy/cache.ts", "../src/lib/mcp-proxy/env-vars.ts", "../src/lib/mcp-proxy/line-stream.ts", "../src/lib/mcp-proxy/upstream.ts"],
|
|
4
|
-
"sourcesContent": ["#!/usr/bin/env node\n// The hashbang must stay on line 1: this entry is published as the `ik-mcp` bin and is exec'd\n// directly by the MCP client, not invoked as `node dist/mcp-proxy.js`.\nimport process from 'node:process'\n\nimport { parseProxyArgv } from 'src/lib/mcp-proxy/argv'\nimport { proxyCacheDir, readBinaryVersion, readProfile, writeProfile } from 'src/lib/mcp-proxy/cache'\nimport type { UpstreamProfile } from 'src/lib/mcp-proxy/cache'\nimport { readListedVars, readUnlistedNames, resolveEnvFilePath } from 'src/lib/mcp-proxy/env-vars'\nimport type { ProxyVars } from 'src/lib/mcp-proxy/env-vars'\nimport { onEachLine } from 'src/lib/mcp-proxy/line-stream'\nimport { createUpstream } from 'src/lib/mcp-proxy/upstream'\nimport type { JsonRpcMessage, ProxySpec, Upstream } from 'src/lib/mcp-proxy/upstream'\n\nconst CREDENTIAL_POLL_MS = Number(process.env.IK_MCP_POLL_MS ?? 2000)\n\n/**\n * Answered on a cold cache, when no upstream has ever been observed on this machine. Pinned rather\n * than echoed from the client: echoing back whatever a client asks for claims support for a\n * revision nothing here has ever spoken. Only `tools` is claimed \u2014 every MCP server has tools, not\n * every one has resources, and claiming `resources` for one that lacks them makes a client issue\n * `resources/list` and get \"method not found\".\n */\nconst FALLBACK_PROTOCOL_VERSION = '2025-06-18'\nconst FALLBACK_CAPABILITIES = { tools: { listChanged: true } }\n\nconst log = (message: string): void => {\n process.stderr.write(`ik-mcp: ${message}\\n`)\n}\n\nconst write = (payload: JsonRpcMessage): void => {\n process.stdout.write(`${JSON.stringify(payload)}\\n`)\n}\n\nconst parsed = parseProxyArgv(process.argv.slice(2))\n\n/**\n * Bad argv is the same state as \"no credentials yet\", not a crash: the handshake is answered, the\n * tool list is empty, and every other request says what is wrong and how to fix it. A proxy that\n * exits on a typo in `.mcp.json` is a dead server the client cannot recover.\n */\nconst degraded: string | null = parsed.ok\n ? null\n : `${parsed.problem} \u2014 run \\`ik setup\\` to regenerate this .mcp.json entry`\nconst spec: ProxySpec | null = parsed.ok ? parsed.spec : null\n\nconst upstream: Upstream | null =\n spec === null\n ? null\n : createUpstream(spec, {\n log,\n // Server-initiated notifications belong to the client and travel verbatim.\n onServerMessage: write,\n // Everything the launching shell picked up from the same env file, minus the listed names,\n // is deleted from the child's inherited environment.\n stripFromChildEnv: () => {\n return readUnlistedNames(spec.env)\n },\n })\n\nconst readVars = (): ProxyVars | null => {\n return spec === null ? null : readListedVars(spec.env)\n}\n\n// Both are resolved on first use, never at module load: `readBinaryVersion` spawns a process and\n// can block for up to its timeout, and doing that before stdin is wired delays the handshake of\n// every session \u2014 including ones that never touch the upstream.\nlet versionCache: string | null = null\nlet profileCache: UpstreamProfile | null | undefined\n\nconst binaryVersion = (): string => {\n versionCache ??=\n // No spec means no upstream to ask, and importing package.json for a version string would drag\n // the whole manifest \u2014 dependency names included \u2014 into a bundle the guard keeps flat.\n spec === null ? 'unknown' : readBinaryVersion(spec.command, ['--version'], process.env.PATH)\n\n return versionCache\n}\n\nconst cacheDir = (): string | null => {\n return spec === null ? null : proxyCacheDir(spec.name)\n}\n\nconst profile = (): UpstreamProfile | null => {\n if (profileCache === undefined) {\n const dir = cacheDir()\n\n profileCache = dir === null ? null : readProfile(binaryVersion(), dir)\n }\n\n return profileCache\n}\n\n/** Fold whatever the live upstream just told us into the on-disk profile. */\nconst rememberProfile = (patch: Partial<UpstreamProfile>): void => {\n const dir = cacheDir()\n\n if (dir === null) return\n\n const current = profile()\n\n const next: UpstreamProfile = {\n version: binaryVersion(),\n protocolVersion: patch.protocolVersion ?? current?.protocolVersion ?? FALLBACK_PROTOCOL_VERSION,\n capabilities: patch.capabilities ?? current?.capabilities ?? FALLBACK_CAPABILITIES,\n tools: patch.tools ?? current?.tools ?? [],\n }\n\n profileCache = next\n writeProfile(next, dir)\n}\n\n/**\n * Answer `initialize` without spawning anything.\n *\n * Spawning here would forfeit lazy start \u2014 a session that never touches the upstream would still\n * pay for a child. The cost is that a first-ever session reports the pinned fallback capabilities\n * and is corrected once a real handshake has been seen; MCP has no capability-change notification,\n * so that correction lands on the NEXT session.\n */\nconst answerInitialize = (message: JsonRpcMessage): void => {\n write({\n jsonrpc: '2.0',\n id: message.id,\n result: {\n protocolVersion: profile()?.protocolVersion ?? FALLBACK_PROTOCOL_VERSION,\n capabilities: profile()?.capabilities ?? FALLBACK_CAPABILITIES,\n serverInfo: { name: spec?.name ?? 'ik-mcp', version: binaryVersion() },\n },\n })\n}\n\nlet announced = false\n\n/**\n * Credentials arrive mid-session, and stdio keeps a server-to-client channel open the whole time \u2014\n * so the moment they appear the client can be told to list again. Polls the env file ONLY: there is\n * no config to poll, so there is no mid-edit \"invalid config\" window and no path by which a watcher\n * could tear down a live upstream.\n */\nconst watchForCredentials = (): void => {\n const timer = setInterval(() => {\n if (announced || readVars() === null) return\n\n announced = true\n clearInterval(timer)\n log('credentials appeared \u2014 announcing tools/list_changed')\n write({ jsonrpc: '2.0', method: 'notifications/tools/list_changed' })\n }, CREDENTIAL_POLL_MS)\n\n timer.unref()\n}\n\nconst forwardToolsList = async (message: JsonRpcMessage, vars: ProxyVars): Promise<void> => {\n const response = await upstream!.request(message, vars)\n const tools = (response.result as { tools?: unknown[] } | undefined)?.tools\n\n if (Array.isArray(tools)) {\n const handshake = upstream!.handshake()?.result as { protocolVersion?: string; capabilities?: unknown } | undefined\n\n rememberProfile({ tools, capabilities: handshake?.capabilities, protocolVersion: handshake?.protocolVersion })\n }\n\n write(response)\n}\n\n/** Before credentials exist, the tool list is answered from cache \u2014 never with an error. */\nconst answerToolsListFromCache = (message: JsonRpcMessage): void => {\n write({ jsonrpc: '2.0', id: message.id, result: { tools: profile()?.tools ?? [] } })\n}\n\n/** Answers the handshake and arms the watcher; deliberately spawns nothing. */\nconst handleInitialize = (message: JsonRpcMessage): void => {\n upstream?.setInitializeParams(message.params)\n answerInitialize(message)\n\n if (degraded !== null) {\n log(`degraded: ${degraded}`)\n\n return\n }\n\n // Always name the resolved path and the spec: when either is wrong (a stale XDG_CACHE_HOME, a\n // session id the client never inherited, a drifted .mcp.json) nothing else in the session says so.\n // Names only \u2014 the VALUES are credentials.\n log(\n `${spec!.name}: env file ${resolveEnvFilePath()}; reads ${spec!.env.join(', ')}; upstream ${spec!.command} ${binaryVersion()}; cache ${profile() ? 'warm' : 'cold'}`,\n )\n\n if (readVars() !== null) {\n announced = true\n\n return\n }\n\n log('no credentials yet \u2014 deferring')\n watchForCredentials()\n}\n\n/** The pre-`env-load` state (or bad argv), which is normal and must never look like a protocol failure. */\nconst answerWithoutCredentials = (message: JsonRpcMessage): void => {\n if (message.method === 'tools/list') {\n answerToolsListFromCache(message)\n\n return\n }\n\n if (message.id == null) return\n\n write({\n jsonrpc: '2.0',\n id: message.id,\n error: {\n code: -32000,\n message:\n degraded ?? `${spec!.name}: credentials are not loaded yet (${spec!.env.join(', ')}) \u2014 run 'ik env-load'`,\n },\n })\n}\n\nconst handle = async (message: JsonRpcMessage): Promise<void> => {\n // An id with no method is a RESPONSE, not a request. A well-behaved client sends none \u2014 the\n // proxy refuses server-initiated requests, so there is nothing to answer \u2014 and treating one as a\n // request would spawn an upstream and reply `no method undefined`.\n if (message.method == null) return\n\n if (message.method === 'initialize') {\n handleInitialize(message)\n\n return\n }\n\n const vars = readVars()\n\n if (vars === null || upstream === null) {\n answerWithoutCredentials(message)\n\n return\n }\n\n if (message.id == null) {\n // Never spawns. `notifications/initialized` in particular is answered by not relaying it at\n // all: every child gets its own from the handshake replay, and forwarding the client's would\n // make the mandatory handshake defeat lazy start.\n if (message.method !== 'notifications/initialized') upstream.notifyIfActive(message)\n\n return\n }\n\n if (message.method === 'tools/list') {\n await forwardToolsList(message, vars)\n\n return\n }\n\n write(await upstream.request(message, vars))\n}\n\nconst onLine = (line: string): void => {\n let message: JsonRpcMessage\n\n try {\n message = JSON.parse(line) as JsonRpcMessage\n } catch {\n log('ignoring non-JSON line from client')\n\n return\n }\n\n handle(message).catch((error: unknown) => {\n const detail = error instanceof Error ? error.message : String(error)\n\n log(detail)\n\n if (message.id != null) {\n write({ jsonrpc: '2.0', id: message.id, error: { code: -32000, message: detail } })\n }\n })\n}\n\nprocess.on('exit', () => {\n upstream?.dispose()\n})\nprocess.on('SIGTERM', () => {\n process.exit(0)\n})\nprocess.on('SIGINT', () => {\n process.exit(0)\n})\n\nonEachLine(process.stdin, onLine)\n\n// The client closing its end is the ordinary way this server ends.\nprocess.stdin.on('end', () => {\n process.exit(0)\n})\n", "import { PROTECTED_CHILD_ENV_NAMES } from './protected-env'\nimport type { ProxySpec } from './upstream'\n\n/** Mirrors the config schema's name rule; re-checked here because the name becomes a cache subdir. */\nconst NAME_PATTERN = /^[a-z][a-z0-9-]*$/\n/** Mirrors `envVarName` in the config schema. */\nconst ENV_VAR_PATTERN = /^[A-Z_]\\w*$/i\n\nexport type ParsedArgv = { ok: true; spec: ProxySpec } | { ok: false; problem: string }\n\ninterface Flags {\n name: string | null\n env: string[]\n unset: string[]\n}\n\n/** Apply one `--flag value` pair, or say why it cannot be applied. */\nconst applyFlag = (flags: Flags, flag: string, value: string): string | null => {\n if (flag === '--name') {\n if (flags.name !== null) return '--name given twice'\n if (!NAME_PATTERN.test(value)) return `--name \"${value}\" must be lowercase letters, digits and hyphens`\n\n flags.name = value\n\n return null\n }\n\n if (flag === '--env' || flag === '--unset') {\n if (!ENV_VAR_PATTERN.test(value)) return `${flag} \"${value}\" is not an environment-variable name`\n // Mirrors the schema refinement, because hand-written argv (the `claude mcp add --scope local`\n // path) never passes through the schema \u2014 and `--unset PATH` would blank PATH and ENOENT the spawn.\n if (PROTECTED_CHILD_ENV_NAMES.has(value)) return `${flag} \"${value}\" is process plumbing, not a credential`\n\n if (flag === '--env') flags.env.push(value)\n else flags.unset.push(value)\n\n return null\n }\n\n return `unknown flag ${flag}`\n}\n\n/** Walk the flag half; returns the index of the `--` separator or a problem. */\nconst readFlags = (argv: readonly string[], flags: Flags): { separator: number } | { problem: string } => {\n let index = 0\n\n while (index < argv.length) {\n const flag = argv[index]!\n\n if (flag === '--') return { separator: index }\n\n // A bare word where a flag belongs is the upstream command arriving early \u2014 the separator\n // is what is missing, and that is the more useful thing to say than \"cmd needs a value\".\n if (!flag.startsWith('--')) return { problem: `missing \"--\" before the upstream command (saw \"${flag}\")` }\n\n const value = argv[index + 1]\n\n // A value that looks like a flag (`--env --`) would otherwise eat the separator and turn the\n // upstream command into a flag \u2014 the case that spawns the wrong binary instead of refusing.\n if (value === undefined || value.startsWith('--')) return { problem: `${flag} needs a value` }\n\n const problem = applyFlag(flags, flag, value)\n\n if (problem !== null) return { problem }\n\n index += 2\n }\n\n return { problem: 'missing \"--\" before the upstream command' }\n}\n\n/**\n * Parse the argv `ik setup` derived: `--name N (--env V)+ (--unset V)* -- <command> [args...]`.\n *\n * Every flag VALUE is validated, not only `--name`. The first `--` in flag position ends our half;\n * everything after it is the upstream's, verbatim, `--`s and all.\n */\n// Node builtins only, on purpose. Pulling commander in here would drag it into the proxy bundle,\n// which the chunk-isolation guard forbids \u2014 and a four-flag grammar does not need it.\nexport const parseProxyArgv = (argv: readonly string[]): ParsedArgv => {\n const flags: Flags = { name: null, env: [], unset: [] }\n const walked = readFlags(argv, flags)\n\n if ('problem' in walked) return { ok: false, problem: walked.problem }\n if (flags.name === null) return { ok: false, problem: 'missing --name' }\n if (flags.env.length === 0) return { ok: false, problem: 'missing --env (at least one variable to read)' }\n\n const command = argv[walked.separator + 1]\n\n if (command === undefined || command.length === 0)\n return { ok: false, problem: 'missing upstream command after \"--\"' }\n\n return {\n ok: true,\n spec: { name: flags.name, command, args: argv.slice(walked.separator + 2), env: flags.env, unset: flags.unset },\n }\n}\n", "import { spawnSync } from 'node:child_process'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport process from 'node:process'\n\nimport { atomicWriteFileSync, getCacheRoot } from 'src/lib/constants'\n\n/**\n * What a session can answer with before any child exists: the tool list and the\n * capability payload the upstream last reported.\n *\n * Capabilities are cached alongside the tools because MCP has no\n * capability-change notification \u2014 a deferred `initialize` must state them up front\n * and can never correct itself, so the honest answer has to come from disk.\n */\nexport interface UpstreamProfile {\n version: string\n protocolVersion: string\n capabilities: unknown\n tools: unknown[]\n}\n\n/**\n * Proxy state lives under the CACHE root, never `~/.infra-kit` \u2014 that is\n * `USER_CONFIG_DIR_NAME`, the auto-seeded project-config registry, where a\n * throwaway derived file has no business living. One subdir per configured name.\n */\nexport const proxyCacheDir = (name: string): string => {\n return path.join(getCacheRoot(), 'mcp-proxy', name)\n}\n\n/** Keeps a version string usable as one path segment. */\nconst safeSegment = (version: string): string => {\n return version.replaceAll(/[^\\w.-]/g, '_').slice(0, 64)\n}\n\nexport const profilePath = (version: string, dir: string): string => {\n return path.join(dir, `profile-${safeSegment(version)}.json`)\n}\n\n/**\n * A cache key for a binary that does not answer `--version`: its resolved path's size and mtime.\n *\n * Both change on every upgrade, which is all the key has to guarantee. Hashing the file would too,\n * but a Go binary is tens of megabytes and this runs at session start.\n */\nexport const binaryFingerprint = (binary: string, searchPath?: string): string => {\n const candidates = path.isAbsolute(binary)\n ? [binary]\n : (searchPath ?? process.env.PATH ?? '').split(path.delimiter).map((dir) => {\n return path.join(dir, binary)\n })\n\n for (const candidate of candidates) {\n try {\n const stat = fs.statSync(candidate)\n\n return `stat-${stat.size}-${Math.trunc(stat.mtimeMs)}`\n } catch {\n // not here; try the next PATH entry\n }\n }\n\n return 'unknown'\n}\n\n/**\n * The upstream binary's self-reported version, falling back to {@link binaryFingerprint}.\n * This is the cache key, so an upgrade that changes the tool set can never be served\n * a stale list \u2014 the new version simply looks at a different file.\n */\nexport const readBinaryVersion = (\n binary: string,\n versionArgs: readonly string[] = ['--version'],\n searchPath?: string,\n): string => {\n try {\n // The same PATH the upstream spawn uses. Without it, a `go install` layout \u2014 where\n // mcp-grafana lives only in GOBIN \u2014 probes as ENOENT, every version collapses into\n // one 'unknown' cache bucket, and an upgrade is served the previous tool list.\n const result = spawnSync(binary, [...versionArgs], {\n encoding: 'utf8',\n timeout: 5000,\n env: searchPath == null ? process.env : { ...process.env, PATH: searchPath },\n })\n\n if (result.status !== 0) return binaryFingerprint(binary, searchPath)\n\n const reported = `${result.stdout}`.trim().split('\\n')[0]?.trim() ?? ''\n\n return reported.length > 0 ? reported : binaryFingerprint(binary, searchPath)\n } catch {\n return binaryFingerprint(binary, searchPath)\n }\n}\n\n/**\n * The cached profile for this binary version, or null.\n *\n * Every failure is a null, never a throw: a torn or truncated file must degrade to\n * \"no cache yet\", because the alternative is an exception on the deferred\n * `initialize` path \u2014 which would kill the session the cache exists to rescue.\n */\nexport const readProfile = (version: string, dir: string): UpstreamProfile | null => {\n try {\n const parsed = JSON.parse(fs.readFileSync(profilePath(version, dir), 'utf8')) as Partial<UpstreamProfile>\n\n if (parsed.version !== version || !Array.isArray(parsed.tools)) return null\n\n return {\n version,\n protocolVersion: typeof parsed.protocolVersion === 'string' ? parsed.protocolVersion : '',\n capabilities: parsed.capabilities ?? {},\n tools: parsed.tools,\n }\n } catch {\n return null\n }\n}\n\n/**\n * Persist a profile. Best-effort: a cache that cannot be written is a slower next\n * session, not a broken one.\n */\nexport const writeProfile = (profile: UpstreamProfile, dir: string): boolean => {\n try {\n fs.mkdirSync(dir, { recursive: true })\n // Atomic, because two sessions can write this concurrently and a half-written\n // file reads back as \"no tools\" \u2014 a silently toolless session with no error.\n atomicWriteFileSync(profilePath(profile.version, dir), `${JSON.stringify(profile)}\\n`, 0o600)\n\n return true\n } catch {\n return false\n }\n}\n", "import path from 'node:path'\nimport process from 'node:process'\n\nimport { ENV_LOAD_FILE, INFRA_KIT_SESSION_VAR, getCacheRoot, parseVarsFromEnvFile } from 'src/lib/constants'\n\n/**\n * Session id used when the proxy inherits none. `getSessionCacheDir()` THROWS in that case,\n * which is correct for a command and fatal for a server \u2014 a proxy that throws at startup is a\n * dead MCP server, so it degrades to a fixed dir instead.\n */\nexport const NO_SESSION = 'no-session'\n\n/** Escape hatch for tests and for pointing the proxy at a hand-written file. */\nexport const ENV_FILE_OVERRIDE_VAR = 'IK_MCP_ENV_FILE'\n\n/** The listed variables' values, keyed by name, in the order the spec listed them. */\nexport type ProxyVars = Record<string, string>\n\nexport const resolveSessionId = (): string => {\n const session = process.env[INFRA_KIT_SESSION_VAR]\n\n return session != null && session.length > 0 ? session : NO_SESSION\n}\n\n/**\n * Where `ik env-load` writes for this session. Goes through `getCacheRoot()` so\n * `XDG_CACHE_HOME` is honoured \u2014 a hardcoded `~/.cache` would silently read a path\n * nothing ever writes on any box that sets it.\n */\nexport const resolveEnvFilePath = (): string => {\n const override = process.env[ENV_FILE_OVERRIDE_VAR]\n\n if (override != null && override.length > 0) return override\n\n return path.join(getCacheRoot(), resolveSessionId(), ENV_LOAD_FILE)\n}\n\n/** The file wins over the inherited environment, per name. */\nconst pick = (fileVars: Record<string, string>, name: string): string => {\n const fromFile = fileVars[name]\n\n if (fromFile != null && fromFile.length > 0) return fromFile\n\n const fromEnv = process.env[name]\n\n return fromEnv != null && fromEnv.length > 0 ? fromEnv : ''\n}\n\n/**\n * The values of `names`, or null while any of them is missing \u2014 the normal state before\n * `ik env-load` runs, which must never be an error.\n *\n * Built by iterating `names`, not the file, so the record's key order \u2014 and therefore its\n * JSON, which is the respawn key \u2014 is stable however Doppler happens to order the file.\n */\nexport const readListedVars = (names: readonly string[], filePath: string = resolveEnvFilePath()): ProxyVars | null => {\n const fileVars = parseVarsFromEnvFile(filePath)\n const vars: ProxyVars = {}\n\n for (const name of names) {\n const value = pick(fileVars, name)\n\n if (value.length === 0) return null\n\n vars[name] = value\n }\n\n return vars\n}\n\n/**\n * Every name the env file defines that the spec does NOT list.\n *\n * The child inherits this process's environment, and this process was launched from a shell\n * that had normally sourced the very file these came from \u2014 so without stripping them by name,\n * every production secret reaches the child by inheritance regardless of what the spec lists.\n */\nexport const readUnlistedNames = (names: readonly string[], filePath: string = resolveEnvFilePath()): string[] => {\n const listed = new Set(names)\n\n return Object.keys(parseVarsFromEnvFile(filePath)).filter((name) => {\n return !listed.has(name)\n })\n}\n", "/**\n * Newline framing for a JSON-RPC stdio stream.\n *\n * Deliberately not `node:readline`. That module is fenced off outside\n * `src/lib/prompts/` because it is the shape a future interactive stdin reader would\n * take, and the prompts module owns the stdin refcount. This is a transport, not a\n * prompt \u2014 but the fence has no exemptions and is worth more intact than this is worth\n * saving, and hand framing is a dozen lines with no buffering semantics to inherit.\n */\nexport const onEachLine = (stream: NodeJS.ReadableStream, visit: (line: string) => void): void => {\n let buffer = ''\n\n stream.setEncoding('utf8')\n stream.on('data', (chunk: string) => {\n buffer += chunk\n\n let boundary = buffer.indexOf('\\n')\n\n while (boundary !== -1) {\n const raw = buffer.slice(0, boundary)\n\n buffer = buffer.slice(boundary + 1)\n\n // A peer that writes CRLF must not hand us a trailing CR inside the JSON.\n const line = raw.endsWith('\\r') ? raw.slice(0, -1) : raw\n\n if (line.trim().length > 0) visit(line)\n\n boundary = buffer.indexOf('\\n')\n }\n })\n}\n", "import type { Buffer } from 'node:buffer'\nimport { spawn } from 'node:child_process'\nimport type { ChildProcessWithoutNullStreams } from 'node:child_process'\nimport os from 'node:os'\nimport path from 'node:path'\nimport process from 'node:process'\n\nimport type { ProxyVars } from './env-vars'\nimport { onEachLine } from './line-stream'\nimport { PROTECTED_CHILD_ENV_NAMES } from './protected-env'\n\nexport interface JsonRpcMessage {\n jsonrpc?: string\n id?: string | number\n method?: string\n params?: unknown\n result?: unknown\n error?: { code: number; message: string }\n}\n\n/**\n * What one configured server is: the argv-borne half of an `mcp.<name>` entry.\n *\n * `env` names are injected inline from {@link ProxyVars}; `unset` names are forced empty so a stale\n * file-based credential can never outrank the inline one (measured for mcp-grafana: an inline token\n * takes precedence over `..._TOKEN_FILE`, but only if the file variable is not also set).\n */\nexport interface ProxySpec {\n name: string\n command: string\n args: readonly string[]\n env: readonly string[]\n unset: readonly string[]\n}\n\nexport interface UpstreamOptions {\n extraEnv?: NodeJS.ProcessEnv\n readyTimeoutMs?: number\n /**\n * Per-request ceiling. A child that answers the handshake and then goes silent must\n * not hang every later request for the rest of the session.\n */\n requestTimeoutMs?: number\n /**\n * Names to DELETE from the child's inherited environment \u2014 the unlisted secrets the\n * launching shell picked up from the same env file.\n */\n stripFromChildEnv?: () => readonly string[]\n log?: (message: string) => void\n /** Notifications and server-initiated requests, which travel to the client verbatim. */\n onServerMessage?: (message: JsonRpcMessage) => void\n}\n\n/** `go install` lands the binary in GOBIN, which a GUI-launched process never has on PATH. */\nconst goBinDir = (): string => {\n const explicit = process.env.GOBIN\n\n if (explicit != null && explicit.length > 0) return explicit\n\n const goPath = process.env.GOPATH\n\n return path.join(goPath != null && goPath.length > 0 ? goPath : path.join(os.homedir(), 'go'), 'bin')\n}\n\nconst DEFAULT_READY_TIMEOUT_MS = 15_000\nconst DEFAULT_REQUEST_TIMEOUT_MS = 120_000\n\nconst NEVER_STRIP = PROTECTED_CHILD_ENV_NAMES\n\ninterface Deferred {\n /** Absent for the shim's OWN requests, whose answers must never reach the client. */\n clientId?: string | number\n resolve: (message: JsonRpcMessage) => void\n reject: (error: Error) => void\n}\n\ninterface Session {\n child: ChildProcessWithoutNullStreams\n /** JSON of the ordered vars record \u2014 the respawn key. */\n key: string\n generation: number\n pending: Map<number, Deferred>\n nextId: number\n handshake: JsonRpcMessage | null\n closed: boolean\n}\n\nexport interface Upstream {\n setInitializeParams: (params: unknown) => void\n request: (message: JsonRpcMessage, vars: ProxyVars) => Promise<JsonRpcMessage>\n /** Forwards only when an upstream already exists; never spawns one. */\n notifyIfActive: (message: JsonRpcMessage) => boolean\n /** The upstream's own `initialize` result, once one has been observed. */\n handshake: () => JsonRpcMessage | null\n dispose: () => void\n}\n\nexport const createUpstream = (spec: ProxySpec, options: UpstreamOptions = {}): Upstream => {\n const binary = spec.command\n const args = spec.args\n const readyTimeoutMs = options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS\n const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS\n const log = options.log ?? ((): void => {})\n const onServerMessage = options.onServerMessage ?? ((): void => {})\n\n const searchPath = `${goBinDir()}${path.delimiter}${process.env.PATH ?? ''}`\n\n let initializeParams: unknown = {}\n let active: Session | null = null\n let starting: { key: string; generation: number; promise: Promise<Session> } | null = null\n let generation = 0\n\n const failPending = (session: Session, reason: string): void => {\n for (const deferred of session.pending.values()) deferred.reject(new Error(reason))\n session.pending.clear()\n }\n\n const closeSession = (session: Session, reason: string): void => {\n if (session.closed) return\n\n session.closed = true\n // In-flight work is ERRORED, never silently dropped: a caller waiting on a killed\n // upstream would otherwise hang for the rest of the session.\n failPending(session, `${spec.name} upstream ${reason}`)\n session.child.kill()\n }\n\n // A response carries an id and no method; anything with a method is server-initiated\n // and belongs to the client. Routing on `method` first is what keeps the shim's own\n // ids from ever being confused with the client's.\n const handleLine = (session: Session, line: string): void => {\n let message: JsonRpcMessage\n\n try {\n message = JSON.parse(line) as JsonRpcMessage\n } catch {\n log(`ignoring non-JSON line from ${binary}`)\n\n return\n }\n\n if (message.method != null) {\n if (message.id == null) {\n onServerMessage(message)\n\n return\n }\n\n // A server-initiated REQUEST carries the upstream's own id. Relaying it would send\n // the client an id from a namespace it does not own, and the client's answer would\n // re-enter as a fresh request with no method. mcp-grafana initiates none today, so\n // refusing is honest and cannot mis-route.\n log(`refusing server-initiated request ${message.method}`)\n send(session, {\n jsonrpc: '2.0',\n id: message.id,\n error: { code: -32601, message: 'client requests are not supported by this shim' },\n })\n\n return\n }\n\n if (message.id == null) return\n\n const deferred = session.pending.get(Number(message.id))\n\n if (!deferred) return\n\n session.pending.delete(Number(message.id))\n\n // Map back: the client only ever sees the id IT chose. Resolving with the wire id\n // would hand the client an answer labelled with the shim's private numbering.\n deferred.resolve(deferred.clientId === undefined ? message : { ...message, id: deferred.clientId })\n }\n\n const send = (session: Session, message: JsonRpcMessage): void => {\n if (session.closed) return\n\n session.child.stdin.write(`${JSON.stringify(message)}\\n`)\n }\n\n /**\n * Send on the shim's OWN id namespace and await the answer.\n *\n * `clientId` is what makes the namespace real: present, the answer is re-labelled and\n * relayed; absent, it is consumed here. The replayed handshake takes the absent form,\n * so a client request numbered `0` can never collide with it.\n */\n const sendRequest = (\n session: Session,\n message: JsonRpcMessage,\n clientId?: string | number,\n ): Promise<JsonRpcMessage> => {\n // A session can be closed between `ensure` resolving and the write landing \u2014 a\n // dispose, a crash, a swap. Registering here anyway would leave the caller pending\n // forever, because the rejection sweep has already run.\n if (session.closed) return Promise.reject(new Error(`${spec.name} upstream is closed`))\n\n const upstreamId = session.nextId\n\n session.nextId += 1\n\n return new Promise<JsonRpcMessage>((resolve, reject) => {\n const timer = setTimeout(() => {\n session.pending.delete(upstreamId)\n reject(new Error(`${binary} did not answer ${message.method ?? 'request'} within ${requestTimeoutMs}ms`))\n }, requestTimeoutMs)\n\n session.pending.set(upstreamId, {\n clientId,\n resolve: (answer: JsonRpcMessage): void => {\n clearTimeout(timer)\n resolve(answer)\n },\n reject: (error: Error): void => {\n clearTimeout(timer)\n reject(error)\n },\n })\n send(session, { ...message, jsonrpc: '2.0', id: upstreamId })\n })\n }\n\n const withDeadline = async (promise: Promise<JsonRpcMessage>): Promise<JsonRpcMessage> => {\n let timer: NodeJS.Timeout | undefined\n\n const deadline = new Promise<never>((_resolve, reject) => {\n timer = setTimeout(() => {\n reject(new Error(`${binary} did not answer initialize within ${readyTimeoutMs}ms`))\n }, readyTimeoutMs)\n })\n\n try {\n return await Promise.race([promise, deadline])\n } finally {\n if (timer) clearTimeout(timer)\n }\n }\n\n const spawnChild = (vars: ProxyVars): ChildProcessWithoutNullStreams => {\n // Listed variables go INLINE \u2014 no credential is written to disk by this proxy at all.\n const childEnv: NodeJS.ProcessEnv = { ...process.env, ...options.extraEnv, PATH: searchPath, ...vars }\n\n // Forced empty rather than deleted: a deleted name would be re-inherited by anything the\n // child spawns from ITS parent's environment; an empty string is an explicit \"no\".\n for (const name of spec.unset) childEnv[name] = ''\n\n // Applied LAST, over the fully merged environment, so no earlier layer can put a\n // secret back. The proxy is normally launched from a shell that sourced the very env\n // file these names came from, and inheritance \u2014 not the listed-names read filter \u2014 is\n // the path by which a production secret would otherwise reach the child.\n for (const name of options.stripFromChildEnv?.() ?? []) {\n if (!NEVER_STRIP.has(name)) delete childEnv[name]\n }\n\n return spawn(binary, [...args], {\n env: childEnv,\n stdio: ['pipe', 'pipe', 'pipe'],\n }) as ChildProcessWithoutNullStreams\n }\n\n const startSession = async (vars: ProxyVars, gen: number): Promise<Session> => {\n const child = spawnChild(vars)\n\n const session: Session = {\n child,\n key: JSON.stringify(vars),\n generation: gen,\n pending: new Map(),\n nextId: 1,\n handshake: null,\n closed: false,\n }\n\n onEachLine(child.stdout, (line) => {\n handleLine(session, line)\n })\n\n child.stderr.on('data', (chunk: Buffer) => {\n // Per LINE, not per chunk: mcp-grafana emits several log lines in one write, and\n // prefixing only the first leaves the rest looking like they came from elsewhere \u2014\n // which matters, because this is the only window into the upstream a user gets.\n for (const line of chunk.toString().trimEnd().split('\\n')) {\n if (line.length > 0) log(`${binary}: ${line}`)\n }\n })\n\n child.on('error', (error: Error) => {\n closeSession(session, `failed to start: ${error.message}`)\n })\n\n child.on('exit', (code) => {\n closeSession(session, `exited with code ${String(code)}`)\n })\n\n try {\n // A child that spawns and never answers must not hang every later request, so the\n // handshake \u2014 not the spawn \u2014 is what readiness means, and it is bounded.\n session.handshake = await withDeadline(sendRequest(session, { method: 'initialize', params: initializeParams }))\n } catch (error) {\n closeSession(session, 'handshake failed')\n throw error\n }\n\n send(session, { jsonrpc: '2.0', method: 'notifications/initialized' })\n log(`serving ${spec.name} (${binary})`)\n\n return session\n }\n\n const ensure = async (vars: ProxyVars): Promise<Session> => {\n const key = JSON.stringify(vars)\n\n if (active && !active.closed && active.key === key) return active\n if (starting && starting.key === key) return starting.promise\n\n generation += 1\n\n const gen = generation\n\n // Which variable changed is deliberately not logged: the values are credentials.\n if (active) log(`${spec.name}: a listed variable changed \u2014 replacing upstream`)\n\n const promise = (async (): Promise<Session> => {\n const session = await startSession(vars, gen)\n\n // A newer target superseded this start while it was in flight. Binding to it now\n // would serve the OLD credentials for the rest of the session.\n if (gen !== generation) {\n closeSession(session, 'superseded by a newer target')\n throw new Error(`${spec.name} upstream superseded by a newer target`)\n }\n\n if (active) closeSession(active, 'replaced')\n active = session\n\n return session\n })()\n\n starting = { key, generation: gen, promise }\n\n try {\n return await promise\n } finally {\n if (starting?.generation === gen) starting = null\n }\n }\n\n return {\n setInitializeParams: (params: unknown): void => {\n initializeParams = params\n },\n request: async (message: JsonRpcMessage, vars: ProxyVars): Promise<JsonRpcMessage> => {\n const session = await ensure(vars)\n\n return sendRequest(session, { method: message.method, params: message.params }, message.id)\n },\n notifyIfActive: (message: JsonRpcMessage): boolean => {\n // Deliberately does NOT call `ensure`. A client notification arriving before any\n // request would otherwise spawn an upstream for a session that has asked the\n // server nothing \u2014 which is exactly what lazy start exists to avoid.\n if (!active || active.closed) return false\n\n send(active, { jsonrpc: '2.0', method: message.method, params: message.params })\n\n return true\n },\n handshake: (): JsonRpcMessage | null => {\n return active?.handshake ?? null\n },\n dispose: (): void => {\n if (active) closeSession(active, 'disposed')\n active = null\n },\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": ["process", "NAME_PATTERN", "ENV_VAR_PATTERN", "applyFlag", "flags", "flag", "value", "PROTECTED_CHILD_ENV_NAMES", "readFlags", "argv", "index", "problem", "parseProxyArgv", "walked", "command", "spawnSync", "fs", "path", "process", "proxyCacheDir", "name", "path", "getCacheRoot", "safeSegment", "version", "profilePath", "dir", "binaryFingerprint", "binary", "searchPath", "candidates", "process", "candidate", "stat", "fs", "readBinaryVersion", "versionArgs", "result", "spawnSync", "reported", "readProfile", "parsed", "writeProfile", "profile", "atomicWriteFileSync", "path", "process", "NO_SESSION", "ENV_FILE_OVERRIDE_VAR", "resolveSessionId", "session", "process", "INFRA_KIT_SESSION_VAR", "resolveEnvFilePath", "override", "path", "getCacheRoot", "ENV_LOAD_FILE", "pick", "fileVars", "name", "fromFile", "fromEnv", "readListedVars", "names", "filePath", "parseVarsFromEnvFile", "vars", "value", "readUnlistedNames", "listed", "onEachLine", "stream", "visit", "buffer", "chunk", "boundary", "raw", "line", "spawn", "os", "path", "process", "goBinDir", "explicit", "process", "goPath", "path", "os", "DEFAULT_READY_TIMEOUT_MS", "DEFAULT_REQUEST_TIMEOUT_MS", "NEVER_STRIP", "PROTECTED_CHILD_ENV_NAMES", "createUpstream", "spec", "options", "binary", "args", "readyTimeoutMs", "requestTimeoutMs", "log", "onServerMessage", "searchPath", "initializeParams", "active", "starting", "generation", "failPending", "session", "reason", "deferred", "closeSession", "handleLine", "line", "message", "send", "sendRequest", "clientId", "upstreamId", "resolve", "reject", "timer", "answer", "error", "withDeadline", "promise", "deadline", "_resolve", "spawnChild", "vars", "childEnv", "name", "spawn", "startSession", "gen", "child", "onEachLine", "chunk", "code", "ensure", "key", "params", "CREDENTIAL_POLL_MS", "process", "FALLBACK_PROTOCOL_VERSION", "FALLBACK_CAPABILITIES", "log", "message", "write", "payload", "parsed", "parseProxyArgv", "degraded", "spec", "upstream", "createUpstream", "readUnlistedNames", "readVars", "readListedVars", "versionCache", "profileCache", "binaryVersion", "readBinaryVersion", "cacheDir", "proxyCacheDir", "profile", "dir", "readProfile", "rememberProfile", "patch", "current", "next", "writeProfile", "answerInitialize", "announced", "watchForCredentials", "timer", "forwardToolsList", "vars", "response", "tools", "handshake", "answerToolsListFromCache", "handleInitialize", "resolveEnvFilePath", "answerWithoutCredentials", "handle", "onLine", "line", "error", "detail", "onEachLine"]
|
|
4
|
+
"sourcesContent": ["#!/usr/bin/env node\n// The hashbang must stay on line 1: this entry is published as the `ik-mcp` bin and is exec'd\n// directly by the MCP client, not invoked as `node dist/mcp-proxy.js`.\nimport process from 'node:process'\n\nimport { bootPortlessLink } from 'src/dev/proxy/portless-link'\nimport { parseProxyArgv } from 'src/lib/mcp-proxy/argv'\nimport { proxyCacheDir, readBinaryVersion, readProfile, writeProfile } from 'src/lib/mcp-proxy/cache'\nimport type { UpstreamProfile } from 'src/lib/mcp-proxy/cache'\nimport { readListedVars, readUnlistedNames, resolveEnvFilePath } from 'src/lib/mcp-proxy/env-vars'\nimport type { ProxyVars } from 'src/lib/mcp-proxy/env-vars'\nimport { onEachLine } from 'src/lib/mcp-proxy/line-stream'\nimport { createUpstream } from 'src/lib/mcp-proxy/upstream'\nimport type { JsonRpcMessage, ProxySpec, Upstream } from 'src/lib/mcp-proxy/upstream'\n\nconst CREDENTIAL_POLL_MS = Number(process.env.IK_MCP_POLL_MS ?? 2000)\n\n/**\n * Answered on a cold cache, when no upstream has ever been observed on this machine. Pinned rather\n * than echoed from the client: echoing back whatever a client asks for claims support for a\n * revision nothing here has ever spoken. Only `tools` is claimed \u2014 every MCP server has tools, not\n * every one has resources, and claiming `resources` for one that lacks them makes a client issue\n * `resources/list` and get \"method not found\".\n */\nconst FALLBACK_PROTOCOL_VERSION = '2025-06-18'\nconst FALLBACK_CAPABILITIES = { tools: { listChanged: true } }\n\nconst log = (message: string): void => {\n process.stderr.write(`ik-mcp: ${message}\\n`)\n}\n\nconst write = (payload: JsonRpcMessage): void => {\n process.stdout.write(`${JSON.stringify(payload)}\\n`)\n}\n\n// This bin is the process Claude Code keeps alive, so it is where `~/.infra-kit/portless` converges on\n// a machine that only ever runs the MCP surface. fs-only and synchronous \u2014 nothing is spawned before the\n// handshake \u2014 and stdout is the transport, so only a failure earns a stderr line; the ordinary outcomes\n// are `doctor`'s to show.\nbootPortlessLink((result, message) => {\n if (result.outcome === 'failed') log(`${message} failed: ${result.link} -> ${result.target}`)\n})\n\nconst parsed = parseProxyArgv(process.argv.slice(2))\n\n/**\n * Bad argv is the same state as \"no credentials yet\", not a crash: the handshake is answered, the\n * tool list is empty, and every other request says what is wrong and how to fix it. A proxy that\n * exits on a typo in `.mcp.json` is a dead server the client cannot recover.\n */\nconst degraded: string | null = parsed.ok\n ? null\n : `${parsed.problem} \u2014 run \\`ik setup\\` to regenerate this .mcp.json entry`\nconst spec: ProxySpec | null = parsed.ok ? parsed.spec : null\n\nconst upstream: Upstream | null =\n spec === null\n ? null\n : createUpstream(spec, {\n log,\n // Server-initiated notifications belong to the client and travel verbatim.\n onServerMessage: write,\n // Everything the launching shell picked up from the same env file, minus the listed names,\n // is deleted from the child's inherited environment.\n stripFromChildEnv: () => {\n return readUnlistedNames(spec.env)\n },\n })\n\nconst readVars = (): ProxyVars | null => {\n return spec === null ? null : readListedVars(spec.env)\n}\n\n// Both are resolved on first use, never at module load: `readBinaryVersion` spawns a process and\n// can block for up to its timeout, and doing that before stdin is wired delays the handshake of\n// every session \u2014 including ones that never touch the upstream.\nlet versionCache: string | null = null\nlet profileCache: UpstreamProfile | null | undefined\n\nconst binaryVersion = (): string => {\n versionCache ??=\n // No spec means no upstream to ask, and importing package.json for a version string would drag\n // the whole manifest \u2014 dependency names included \u2014 into a bundle the guard keeps flat.\n spec === null ? 'unknown' : readBinaryVersion(spec.command, ['--version'], process.env.PATH)\n\n return versionCache\n}\n\nconst cacheDir = (): string | null => {\n return spec === null ? null : proxyCacheDir(spec.name)\n}\n\nconst profile = (): UpstreamProfile | null => {\n if (profileCache === undefined) {\n const dir = cacheDir()\n\n profileCache = dir === null ? null : readProfile(binaryVersion(), dir)\n }\n\n return profileCache\n}\n\n/** Fold whatever the live upstream just told us into the on-disk profile. */\nconst rememberProfile = (patch: Partial<UpstreamProfile>): void => {\n const dir = cacheDir()\n\n if (dir === null) return\n\n const current = profile()\n\n const next: UpstreamProfile = {\n version: binaryVersion(),\n protocolVersion: patch.protocolVersion ?? current?.protocolVersion ?? FALLBACK_PROTOCOL_VERSION,\n capabilities: patch.capabilities ?? current?.capabilities ?? FALLBACK_CAPABILITIES,\n tools: patch.tools ?? current?.tools ?? [],\n }\n\n profileCache = next\n writeProfile(next, dir)\n}\n\n/**\n * Answer `initialize` without spawning anything.\n *\n * Spawning here would forfeit lazy start \u2014 a session that never touches the upstream would still\n * pay for a child. The cost is that a first-ever session reports the pinned fallback capabilities\n * and is corrected once a real handshake has been seen; MCP has no capability-change notification,\n * so that correction lands on the NEXT session.\n */\nconst answerInitialize = (message: JsonRpcMessage): void => {\n write({\n jsonrpc: '2.0',\n id: message.id,\n result: {\n protocolVersion: profile()?.protocolVersion ?? FALLBACK_PROTOCOL_VERSION,\n capabilities: profile()?.capabilities ?? FALLBACK_CAPABILITIES,\n serverInfo: { name: spec?.name ?? 'ik-mcp', version: binaryVersion() },\n },\n })\n}\n\nlet announced = false\n\n/**\n * Credentials arrive mid-session, and stdio keeps a server-to-client channel open the whole time \u2014\n * so the moment they appear the client can be told to list again. Polls the env file ONLY: there is\n * no config to poll, so there is no mid-edit \"invalid config\" window and no path by which a watcher\n * could tear down a live upstream.\n */\nconst watchForCredentials = (): void => {\n const timer = setInterval(() => {\n if (announced || readVars() === null) return\n\n announced = true\n clearInterval(timer)\n log('credentials appeared \u2014 announcing tools/list_changed')\n write({ jsonrpc: '2.0', method: 'notifications/tools/list_changed' })\n }, CREDENTIAL_POLL_MS)\n\n timer.unref()\n}\n\nconst forwardToolsList = async (message: JsonRpcMessage, vars: ProxyVars): Promise<void> => {\n const response = await upstream!.request(message, vars)\n const tools = (response.result as { tools?: unknown[] } | undefined)?.tools\n\n if (Array.isArray(tools)) {\n const handshake = upstream!.handshake()?.result as { protocolVersion?: string; capabilities?: unknown } | undefined\n\n rememberProfile({ tools, capabilities: handshake?.capabilities, protocolVersion: handshake?.protocolVersion })\n }\n\n write(response)\n}\n\n/** Before credentials exist, the tool list is answered from cache \u2014 never with an error. */\nconst answerToolsListFromCache = (message: JsonRpcMessage): void => {\n write({ jsonrpc: '2.0', id: message.id, result: { tools: profile()?.tools ?? [] } })\n}\n\n/** Answers the handshake and arms the watcher; deliberately spawns nothing. */\nconst handleInitialize = (message: JsonRpcMessage): void => {\n upstream?.setInitializeParams(message.params)\n answerInitialize(message)\n\n if (degraded !== null) {\n log(`degraded: ${degraded}`)\n\n return\n }\n\n // Always name the resolved path and the spec: when either is wrong (a stale XDG_CACHE_HOME, a\n // session id the client never inherited, a drifted .mcp.json) nothing else in the session says so.\n // Names only \u2014 the VALUES are credentials.\n log(\n `${spec!.name}: env file ${resolveEnvFilePath()}; reads ${spec!.env.join(', ')}; upstream ${spec!.command} ${binaryVersion()}; cache ${profile() ? 'warm' : 'cold'}`,\n )\n\n if (readVars() !== null) {\n announced = true\n\n return\n }\n\n log('no credentials yet \u2014 deferring')\n watchForCredentials()\n}\n\n/** The pre-`env-load` state (or bad argv), which is normal and must never look like a protocol failure. */\nconst answerWithoutCredentials = (message: JsonRpcMessage): void => {\n if (message.method === 'tools/list') {\n answerToolsListFromCache(message)\n\n return\n }\n\n if (message.id == null) return\n\n write({\n jsonrpc: '2.0',\n id: message.id,\n error: {\n code: -32000,\n message:\n degraded ?? `${spec!.name}: credentials are not loaded yet (${spec!.env.join(', ')}) \u2014 run 'ik env-load'`,\n },\n })\n}\n\nconst handle = async (message: JsonRpcMessage): Promise<void> => {\n // An id with no method is a RESPONSE, not a request. A well-behaved client sends none \u2014 the\n // proxy refuses server-initiated requests, so there is nothing to answer \u2014 and treating one as a\n // request would spawn an upstream and reply `no method undefined`.\n if (message.method == null) return\n\n if (message.method === 'initialize') {\n handleInitialize(message)\n\n return\n }\n\n const vars = readVars()\n\n if (vars === null || upstream === null) {\n answerWithoutCredentials(message)\n\n return\n }\n\n if (message.id == null) {\n // Never spawns. `notifications/initialized` in particular is answered by not relaying it at\n // all: every child gets its own from the handshake replay, and forwarding the client's would\n // make the mandatory handshake defeat lazy start.\n if (message.method !== 'notifications/initialized') upstream.notifyIfActive(message)\n\n return\n }\n\n if (message.method === 'tools/list') {\n await forwardToolsList(message, vars)\n\n return\n }\n\n write(await upstream.request(message, vars))\n}\n\nconst onLine = (line: string): void => {\n let message: JsonRpcMessage\n\n try {\n message = JSON.parse(line) as JsonRpcMessage\n } catch {\n log('ignoring non-JSON line from client')\n\n return\n }\n\n handle(message).catch((error: unknown) => {\n const detail = error instanceof Error ? error.message : String(error)\n\n log(detail)\n\n if (message.id != null) {\n write({ jsonrpc: '2.0', id: message.id, error: { code: -32000, message: detail } })\n }\n })\n}\n\nprocess.on('exit', () => {\n upstream?.dispose()\n})\nprocess.on('SIGTERM', () => {\n process.exit(0)\n})\nprocess.on('SIGINT', () => {\n process.exit(0)\n})\n\nonEachLine(process.stdin, onLine)\n\n// The client closing its end is the ordinary way this server ends.\nprocess.stdin.on('end', () => {\n process.exit(0)\n})\n", "import { PROTECTED_CHILD_ENV_NAMES } from './protected-env'\nimport type { ProxySpec } from './upstream'\n\n/** Mirrors the config schema's name rule; re-checked here because the name becomes a cache subdir. */\nconst NAME_PATTERN = /^[a-z][a-z0-9-]*$/\n/** Mirrors `envVarName` in the config schema. */\nconst ENV_VAR_PATTERN = /^[A-Z_]\\w*$/i\n\nexport type ParsedArgv = { ok: true; spec: ProxySpec } | { ok: false; problem: string }\n\ninterface Flags {\n name: string | null\n env: string[]\n unset: string[]\n}\n\n/** Apply one `--flag value` pair, or say why it cannot be applied. */\nconst applyFlag = (flags: Flags, flag: string, value: string): string | null => {\n if (flag === '--name') {\n if (flags.name !== null) return '--name given twice'\n if (!NAME_PATTERN.test(value)) return `--name \"${value}\" must be lowercase letters, digits and hyphens`\n\n flags.name = value\n\n return null\n }\n\n if (flag === '--env' || flag === '--unset') {\n if (!ENV_VAR_PATTERN.test(value)) return `${flag} \"${value}\" is not an environment-variable name`\n // Mirrors the schema refinement, because hand-written argv (the `claude mcp add --scope local`\n // path) never passes through the schema \u2014 and `--unset PATH` would blank PATH and ENOENT the spawn.\n if (PROTECTED_CHILD_ENV_NAMES.has(value)) return `${flag} \"${value}\" is process plumbing, not a credential`\n\n if (flag === '--env') flags.env.push(value)\n else flags.unset.push(value)\n\n return null\n }\n\n return `unknown flag ${flag}`\n}\n\n/** Walk the flag half; returns the index of the `--` separator or a problem. */\nconst readFlags = (argv: readonly string[], flags: Flags): { separator: number } | { problem: string } => {\n let index = 0\n\n while (index < argv.length) {\n const flag = argv[index]!\n\n if (flag === '--') return { separator: index }\n\n // A bare word where a flag belongs is the upstream command arriving early \u2014 the separator\n // is what is missing, and that is the more useful thing to say than \"cmd needs a value\".\n if (!flag.startsWith('--')) return { problem: `missing \"--\" before the upstream command (saw \"${flag}\")` }\n\n const value = argv[index + 1]\n\n // A value that looks like a flag (`--env --`) would otherwise eat the separator and turn the\n // upstream command into a flag \u2014 the case that spawns the wrong binary instead of refusing.\n if (value === undefined || value.startsWith('--')) return { problem: `${flag} needs a value` }\n\n const problem = applyFlag(flags, flag, value)\n\n if (problem !== null) return { problem }\n\n index += 2\n }\n\n return { problem: 'missing \"--\" before the upstream command' }\n}\n\n/**\n * Parse the argv `ik setup` derived: `--name N (--env V)+ (--unset V)* -- <command> [args...]`.\n *\n * Every flag VALUE is validated, not only `--name`. The first `--` in flag position ends our half;\n * everything after it is the upstream's, verbatim, `--`s and all.\n */\n// Node builtins only, on purpose. Pulling commander in here would drag it into the proxy bundle,\n// which the chunk-isolation guard forbids \u2014 and a four-flag grammar does not need it.\nexport const parseProxyArgv = (argv: readonly string[]): ParsedArgv => {\n const flags: Flags = { name: null, env: [], unset: [] }\n const walked = readFlags(argv, flags)\n\n if ('problem' in walked) return { ok: false, problem: walked.problem }\n if (flags.name === null) return { ok: false, problem: 'missing --name' }\n if (flags.env.length === 0) return { ok: false, problem: 'missing --env (at least one variable to read)' }\n\n const command = argv[walked.separator + 1]\n\n if (command === undefined || command.length === 0)\n return { ok: false, problem: 'missing upstream command after \"--\"' }\n\n return {\n ok: true,\n spec: { name: flags.name, command, args: argv.slice(walked.separator + 2), env: flags.env, unset: flags.unset },\n }\n}\n", "import { spawnSync } from 'node:child_process'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport process from 'node:process'\n\nimport { atomicWriteFileSync, getCacheRoot } from 'src/lib/constants'\n\n/**\n * What a session can answer with before any child exists: the tool list and the\n * capability payload the upstream last reported.\n *\n * Capabilities are cached alongside the tools because MCP has no\n * capability-change notification \u2014 a deferred `initialize` must state them up front\n * and can never correct itself, so the honest answer has to come from disk.\n */\nexport interface UpstreamProfile {\n version: string\n protocolVersion: string\n capabilities: unknown\n tools: unknown[]\n}\n\n/**\n * Proxy state lives under the CACHE root, never `~/.infra-kit` \u2014 that is\n * `USER_CONFIG_DIR_NAME`, the auto-seeded project-config registry, where a\n * throwaway derived file has no business living. One subdir per configured name.\n */\nexport const proxyCacheDir = (name: string): string => {\n return path.join(getCacheRoot(), 'mcp-proxy', name)\n}\n\n/** Keeps a version string usable as one path segment. */\nconst safeSegment = (version: string): string => {\n return version.replaceAll(/[^\\w.-]/g, '_').slice(0, 64)\n}\n\nexport const profilePath = (version: string, dir: string): string => {\n return path.join(dir, `profile-${safeSegment(version)}.json`)\n}\n\n/**\n * A cache key for a binary that does not answer `--version`: its resolved path's size and mtime.\n *\n * Both change on every upgrade, which is all the key has to guarantee. Hashing the file would too,\n * but a Go binary is tens of megabytes and this runs at session start.\n */\nexport const binaryFingerprint = (binary: string, searchPath?: string): string => {\n const candidates = path.isAbsolute(binary)\n ? [binary]\n : (searchPath ?? process.env.PATH ?? '').split(path.delimiter).map((dir) => {\n return path.join(dir, binary)\n })\n\n for (const candidate of candidates) {\n try {\n const stat = fs.statSync(candidate)\n\n return `stat-${stat.size}-${Math.trunc(stat.mtimeMs)}`\n } catch {\n // not here; try the next PATH entry\n }\n }\n\n return 'unknown'\n}\n\n/**\n * The upstream binary's self-reported version, falling back to {@link binaryFingerprint}.\n * This is the cache key, so an upgrade that changes the tool set can never be served\n * a stale list \u2014 the new version simply looks at a different file.\n */\nexport const readBinaryVersion = (\n binary: string,\n versionArgs: readonly string[] = ['--version'],\n searchPath?: string,\n): string => {\n try {\n // The same PATH the upstream spawn uses. Without it, a `go install` layout \u2014 where\n // mcp-grafana lives only in GOBIN \u2014 probes as ENOENT, every version collapses into\n // one 'unknown' cache bucket, and an upgrade is served the previous tool list.\n const result = spawnSync(binary, [...versionArgs], {\n encoding: 'utf8',\n timeout: 5000,\n env: searchPath == null ? process.env : { ...process.env, PATH: searchPath },\n })\n\n if (result.status !== 0) return binaryFingerprint(binary, searchPath)\n\n const reported = `${result.stdout}`.trim().split('\\n')[0]?.trim() ?? ''\n\n return reported.length > 0 ? reported : binaryFingerprint(binary, searchPath)\n } catch {\n return binaryFingerprint(binary, searchPath)\n }\n}\n\n/**\n * The cached profile for this binary version, or null.\n *\n * Every failure is a null, never a throw: a torn or truncated file must degrade to\n * \"no cache yet\", because the alternative is an exception on the deferred\n * `initialize` path \u2014 which would kill the session the cache exists to rescue.\n */\nexport const readProfile = (version: string, dir: string): UpstreamProfile | null => {\n try {\n const parsed = JSON.parse(fs.readFileSync(profilePath(version, dir), 'utf8')) as Partial<UpstreamProfile>\n\n if (parsed.version !== version || !Array.isArray(parsed.tools)) return null\n\n return {\n version,\n protocolVersion: typeof parsed.protocolVersion === 'string' ? parsed.protocolVersion : '',\n capabilities: parsed.capabilities ?? {},\n tools: parsed.tools,\n }\n } catch {\n return null\n }\n}\n\n/**\n * Persist a profile. Best-effort: a cache that cannot be written is a slower next\n * session, not a broken one.\n */\nexport const writeProfile = (profile: UpstreamProfile, dir: string): boolean => {\n try {\n fs.mkdirSync(dir, { recursive: true })\n // Atomic, because two sessions can write this concurrently and a half-written\n // file reads back as \"no tools\" \u2014 a silently toolless session with no error.\n atomicWriteFileSync(profilePath(profile.version, dir), `${JSON.stringify(profile)}\\n`, 0o600)\n\n return true\n } catch {\n return false\n }\n}\n", "import path from 'node:path'\nimport process from 'node:process'\n\nimport { ENV_LOAD_FILE, INFRA_KIT_SESSION_VAR, getCacheRoot, parseVarsFromEnvFile } from 'src/lib/constants'\n\n/**\n * Session id used when the proxy inherits none. `getSessionCacheDir()` THROWS in that case,\n * which is correct for a command and fatal for a server \u2014 a proxy that throws at startup is a\n * dead MCP server, so it degrades to a fixed dir instead.\n */\nexport const NO_SESSION = 'no-session'\n\n/** Escape hatch for tests and for pointing the proxy at a hand-written file. */\nexport const ENV_FILE_OVERRIDE_VAR = 'IK_MCP_ENV_FILE'\n\n/** The listed variables' values, keyed by name, in the order the spec listed them. */\nexport type ProxyVars = Record<string, string>\n\nexport const resolveSessionId = (): string => {\n const session = process.env[INFRA_KIT_SESSION_VAR]\n\n return session != null && session.length > 0 ? session : NO_SESSION\n}\n\n/**\n * Where `ik env-load` writes for this session. Goes through `getCacheRoot()` so\n * `XDG_CACHE_HOME` is honoured \u2014 a hardcoded `~/.cache` would silently read a path\n * nothing ever writes on any box that sets it.\n */\nexport const resolveEnvFilePath = (): string => {\n const override = process.env[ENV_FILE_OVERRIDE_VAR]\n\n if (override != null && override.length > 0) return override\n\n return path.join(getCacheRoot(), resolveSessionId(), ENV_LOAD_FILE)\n}\n\n/** The file wins over the inherited environment, per name. */\nconst pick = (fileVars: Record<string, string>, name: string): string => {\n const fromFile = fileVars[name]\n\n if (fromFile != null && fromFile.length > 0) return fromFile\n\n const fromEnv = process.env[name]\n\n return fromEnv != null && fromEnv.length > 0 ? fromEnv : ''\n}\n\n/**\n * The values of `names`, or null while any of them is missing \u2014 the normal state before\n * `ik env-load` runs, which must never be an error.\n *\n * Built by iterating `names`, not the file, so the record's key order \u2014 and therefore its\n * JSON, which is the respawn key \u2014 is stable however Doppler happens to order the file.\n */\nexport const readListedVars = (names: readonly string[], filePath: string = resolveEnvFilePath()): ProxyVars | null => {\n const fileVars = parseVarsFromEnvFile(filePath)\n const vars: ProxyVars = {}\n\n for (const name of names) {\n const value = pick(fileVars, name)\n\n if (value.length === 0) return null\n\n vars[name] = value\n }\n\n return vars\n}\n\n/**\n * Every name the env file defines that the spec does NOT list.\n *\n * The child inherits this process's environment, and this process was launched from a shell\n * that had normally sourced the very file these came from \u2014 so without stripping them by name,\n * every production secret reaches the child by inheritance regardless of what the spec lists.\n */\nexport const readUnlistedNames = (names: readonly string[], filePath: string = resolveEnvFilePath()): string[] => {\n const listed = new Set(names)\n\n return Object.keys(parseVarsFromEnvFile(filePath)).filter((name) => {\n return !listed.has(name)\n })\n}\n", "/**\n * Newline framing for a JSON-RPC stdio stream.\n *\n * Deliberately not `node:readline`. That module is fenced off outside\n * `src/lib/prompts/` because it is the shape a future interactive stdin reader would\n * take, and the prompts module owns the stdin refcount. This is a transport, not a\n * prompt \u2014 but the fence has no exemptions and is worth more intact than this is worth\n * saving, and hand framing is a dozen lines with no buffering semantics to inherit.\n */\nexport const onEachLine = (stream: NodeJS.ReadableStream, visit: (line: string) => void): void => {\n let buffer = ''\n\n stream.setEncoding('utf8')\n stream.on('data', (chunk: string) => {\n buffer += chunk\n\n let boundary = buffer.indexOf('\\n')\n\n while (boundary !== -1) {\n const raw = buffer.slice(0, boundary)\n\n buffer = buffer.slice(boundary + 1)\n\n // A peer that writes CRLF must not hand us a trailing CR inside the JSON.\n const line = raw.endsWith('\\r') ? raw.slice(0, -1) : raw\n\n if (line.trim().length > 0) visit(line)\n\n boundary = buffer.indexOf('\\n')\n }\n })\n}\n", "import type { Buffer } from 'node:buffer'\nimport { spawn } from 'node:child_process'\nimport type { ChildProcessWithoutNullStreams } from 'node:child_process'\nimport os from 'node:os'\nimport path from 'node:path'\nimport process from 'node:process'\n\nimport type { ProxyVars } from './env-vars'\nimport { onEachLine } from './line-stream'\nimport { PROTECTED_CHILD_ENV_NAMES } from './protected-env'\n\nexport interface JsonRpcMessage {\n jsonrpc?: string\n id?: string | number\n method?: string\n params?: unknown\n result?: unknown\n error?: { code: number; message: string }\n}\n\n/**\n * What one configured server is: the argv-borne half of an `mcp.<name>` entry.\n *\n * `env` names are injected inline from {@link ProxyVars}; `unset` names are forced empty so a stale\n * file-based credential can never outrank the inline one (measured for mcp-grafana: an inline token\n * takes precedence over `..._TOKEN_FILE`, but only if the file variable is not also set).\n */\nexport interface ProxySpec {\n name: string\n command: string\n args: readonly string[]\n env: readonly string[]\n unset: readonly string[]\n}\n\nexport interface UpstreamOptions {\n extraEnv?: NodeJS.ProcessEnv\n readyTimeoutMs?: number\n /**\n * Per-request ceiling. A child that answers the handshake and then goes silent must\n * not hang every later request for the rest of the session.\n */\n requestTimeoutMs?: number\n /**\n * Names to DELETE from the child's inherited environment \u2014 the unlisted secrets the\n * launching shell picked up from the same env file.\n */\n stripFromChildEnv?: () => readonly string[]\n log?: (message: string) => void\n /** Notifications and server-initiated requests, which travel to the client verbatim. */\n onServerMessage?: (message: JsonRpcMessage) => void\n}\n\n/** `go install` lands the binary in GOBIN, which a GUI-launched process never has on PATH. */\nconst goBinDir = (): string => {\n const explicit = process.env.GOBIN\n\n if (explicit != null && explicit.length > 0) return explicit\n\n const goPath = process.env.GOPATH\n\n return path.join(goPath != null && goPath.length > 0 ? goPath : path.join(os.homedir(), 'go'), 'bin')\n}\n\nconst DEFAULT_READY_TIMEOUT_MS = 15_000\nconst DEFAULT_REQUEST_TIMEOUT_MS = 120_000\n\nconst NEVER_STRIP = PROTECTED_CHILD_ENV_NAMES\n\ninterface Deferred {\n /** Absent for the shim's OWN requests, whose answers must never reach the client. */\n clientId?: string | number\n resolve: (message: JsonRpcMessage) => void\n reject: (error: Error) => void\n}\n\ninterface Session {\n child: ChildProcessWithoutNullStreams\n /** JSON of the ordered vars record \u2014 the respawn key. */\n key: string\n generation: number\n pending: Map<number, Deferred>\n nextId: number\n handshake: JsonRpcMessage | null\n closed: boolean\n}\n\nexport interface Upstream {\n setInitializeParams: (params: unknown) => void\n request: (message: JsonRpcMessage, vars: ProxyVars) => Promise<JsonRpcMessage>\n /** Forwards only when an upstream already exists; never spawns one. */\n notifyIfActive: (message: JsonRpcMessage) => boolean\n /** The upstream's own `initialize` result, once one has been observed. */\n handshake: () => JsonRpcMessage | null\n dispose: () => void\n}\n\nexport const createUpstream = (spec: ProxySpec, options: UpstreamOptions = {}): Upstream => {\n const binary = spec.command\n const args = spec.args\n const readyTimeoutMs = options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS\n const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS\n const log = options.log ?? ((): void => {})\n const onServerMessage = options.onServerMessage ?? ((): void => {})\n\n const searchPath = `${goBinDir()}${path.delimiter}${process.env.PATH ?? ''}`\n\n let initializeParams: unknown = {}\n let active: Session | null = null\n let starting: { key: string; generation: number; promise: Promise<Session> } | null = null\n let generation = 0\n\n const failPending = (session: Session, reason: string): void => {\n for (const deferred of session.pending.values()) deferred.reject(new Error(reason))\n session.pending.clear()\n }\n\n const closeSession = (session: Session, reason: string): void => {\n if (session.closed) return\n\n session.closed = true\n // In-flight work is ERRORED, never silently dropped: a caller waiting on a killed\n // upstream would otherwise hang for the rest of the session.\n failPending(session, `${spec.name} upstream ${reason}`)\n session.child.kill()\n }\n\n // A response carries an id and no method; anything with a method is server-initiated\n // and belongs to the client. Routing on `method` first is what keeps the shim's own\n // ids from ever being confused with the client's.\n const handleLine = (session: Session, line: string): void => {\n let message: JsonRpcMessage\n\n try {\n message = JSON.parse(line) as JsonRpcMessage\n } catch {\n log(`ignoring non-JSON line from ${binary}`)\n\n return\n }\n\n if (message.method != null) {\n if (message.id == null) {\n onServerMessage(message)\n\n return\n }\n\n // A server-initiated REQUEST carries the upstream's own id. Relaying it would send\n // the client an id from a namespace it does not own, and the client's answer would\n // re-enter as a fresh request with no method. mcp-grafana initiates none today, so\n // refusing is honest and cannot mis-route.\n log(`refusing server-initiated request ${message.method}`)\n send(session, {\n jsonrpc: '2.0',\n id: message.id,\n error: { code: -32601, message: 'client requests are not supported by this shim' },\n })\n\n return\n }\n\n if (message.id == null) return\n\n const deferred = session.pending.get(Number(message.id))\n\n if (!deferred) return\n\n session.pending.delete(Number(message.id))\n\n // Map back: the client only ever sees the id IT chose. Resolving with the wire id\n // would hand the client an answer labelled with the shim's private numbering.\n deferred.resolve(deferred.clientId === undefined ? message : { ...message, id: deferred.clientId })\n }\n\n const send = (session: Session, message: JsonRpcMessage): void => {\n if (session.closed) return\n\n session.child.stdin.write(`${JSON.stringify(message)}\\n`)\n }\n\n /**\n * Send on the shim's OWN id namespace and await the answer.\n *\n * `clientId` is what makes the namespace real: present, the answer is re-labelled and\n * relayed; absent, it is consumed here. The replayed handshake takes the absent form,\n * so a client request numbered `0` can never collide with it.\n */\n const sendRequest = (\n session: Session,\n message: JsonRpcMessage,\n clientId?: string | number,\n ): Promise<JsonRpcMessage> => {\n // A session can be closed between `ensure` resolving and the write landing \u2014 a\n // dispose, a crash, a swap. Registering here anyway would leave the caller pending\n // forever, because the rejection sweep has already run.\n if (session.closed) return Promise.reject(new Error(`${spec.name} upstream is closed`))\n\n const upstreamId = session.nextId\n\n session.nextId += 1\n\n return new Promise<JsonRpcMessage>((resolve, reject) => {\n const timer = setTimeout(() => {\n session.pending.delete(upstreamId)\n reject(new Error(`${binary} did not answer ${message.method ?? 'request'} within ${requestTimeoutMs}ms`))\n }, requestTimeoutMs)\n\n session.pending.set(upstreamId, {\n clientId,\n resolve: (answer: JsonRpcMessage): void => {\n clearTimeout(timer)\n resolve(answer)\n },\n reject: (error: Error): void => {\n clearTimeout(timer)\n reject(error)\n },\n })\n send(session, { ...message, jsonrpc: '2.0', id: upstreamId })\n })\n }\n\n const withDeadline = async (promise: Promise<JsonRpcMessage>): Promise<JsonRpcMessage> => {\n let timer: NodeJS.Timeout | undefined\n\n const deadline = new Promise<never>((_resolve, reject) => {\n timer = setTimeout(() => {\n reject(new Error(`${binary} did not answer initialize within ${readyTimeoutMs}ms`))\n }, readyTimeoutMs)\n })\n\n try {\n return await Promise.race([promise, deadline])\n } finally {\n if (timer) clearTimeout(timer)\n }\n }\n\n const spawnChild = (vars: ProxyVars): ChildProcessWithoutNullStreams => {\n // Listed variables go INLINE \u2014 no credential is written to disk by this proxy at all.\n const childEnv: NodeJS.ProcessEnv = { ...process.env, ...options.extraEnv, PATH: searchPath, ...vars }\n\n // Forced empty rather than deleted: a deleted name would be re-inherited by anything the\n // child spawns from ITS parent's environment; an empty string is an explicit \"no\".\n for (const name of spec.unset) childEnv[name] = ''\n\n // Applied LAST, over the fully merged environment, so no earlier layer can put a\n // secret back. The proxy is normally launched from a shell that sourced the very env\n // file these names came from, and inheritance \u2014 not the listed-names read filter \u2014 is\n // the path by which a production secret would otherwise reach the child.\n for (const name of options.stripFromChildEnv?.() ?? []) {\n if (!NEVER_STRIP.has(name)) delete childEnv[name]\n }\n\n return spawn(binary, [...args], {\n env: childEnv,\n stdio: ['pipe', 'pipe', 'pipe'],\n }) as ChildProcessWithoutNullStreams\n }\n\n const startSession = async (vars: ProxyVars, gen: number): Promise<Session> => {\n const child = spawnChild(vars)\n\n const session: Session = {\n child,\n key: JSON.stringify(vars),\n generation: gen,\n pending: new Map(),\n nextId: 1,\n handshake: null,\n closed: false,\n }\n\n onEachLine(child.stdout, (line) => {\n handleLine(session, line)\n })\n\n child.stderr.on('data', (chunk: Buffer) => {\n // Per LINE, not per chunk: mcp-grafana emits several log lines in one write, and\n // prefixing only the first leaves the rest looking like they came from elsewhere \u2014\n // which matters, because this is the only window into the upstream a user gets.\n for (const line of chunk.toString().trimEnd().split('\\n')) {\n if (line.length > 0) log(`${binary}: ${line}`)\n }\n })\n\n child.on('error', (error: Error) => {\n closeSession(session, `failed to start: ${error.message}`)\n })\n\n child.on('exit', (code) => {\n closeSession(session, `exited with code ${String(code)}`)\n })\n\n try {\n // A child that spawns and never answers must not hang every later request, so the\n // handshake \u2014 not the spawn \u2014 is what readiness means, and it is bounded.\n session.handshake = await withDeadline(sendRequest(session, { method: 'initialize', params: initializeParams }))\n } catch (error) {\n closeSession(session, 'handshake failed')\n throw error\n }\n\n send(session, { jsonrpc: '2.0', method: 'notifications/initialized' })\n log(`serving ${spec.name} (${binary})`)\n\n return session\n }\n\n const ensure = async (vars: ProxyVars): Promise<Session> => {\n const key = JSON.stringify(vars)\n\n if (active && !active.closed && active.key === key) return active\n if (starting && starting.key === key) return starting.promise\n\n generation += 1\n\n const gen = generation\n\n // Which variable changed is deliberately not logged: the values are credentials.\n if (active) log(`${spec.name}: a listed variable changed \u2014 replacing upstream`)\n\n const promise = (async (): Promise<Session> => {\n const session = await startSession(vars, gen)\n\n // A newer target superseded this start while it was in flight. Binding to it now\n // would serve the OLD credentials for the rest of the session.\n if (gen !== generation) {\n closeSession(session, 'superseded by a newer target')\n throw new Error(`${spec.name} upstream superseded by a newer target`)\n }\n\n if (active) closeSession(active, 'replaced')\n active = session\n\n return session\n })()\n\n starting = { key, generation: gen, promise }\n\n try {\n return await promise\n } finally {\n if (starting?.generation === gen) starting = null\n }\n }\n\n return {\n setInitializeParams: (params: unknown): void => {\n initializeParams = params\n },\n request: async (message: JsonRpcMessage, vars: ProxyVars): Promise<JsonRpcMessage> => {\n const session = await ensure(vars)\n\n return sendRequest(session, { method: message.method, params: message.params }, message.id)\n },\n notifyIfActive: (message: JsonRpcMessage): boolean => {\n // Deliberately does NOT call `ensure`. A client notification arriving before any\n // request would otherwise spawn an upstream for a session that has asked the\n // server nothing \u2014 which is exactly what lazy start exists to avoid.\n if (!active || active.closed) return false\n\n send(active, { jsonrpc: '2.0', method: message.method, params: message.params })\n\n return true\n },\n handshake: (): JsonRpcMessage | null => {\n return active?.handshake ?? null\n },\n dispose: (): void => {\n if (active) closeSession(active, 'disposed')\n active = null\n },\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["process", "NAME_PATTERN", "ENV_VAR_PATTERN", "applyFlag", "flags", "flag", "value", "PROTECTED_CHILD_ENV_NAMES", "readFlags", "argv", "index", "problem", "parseProxyArgv", "walked", "command", "spawnSync", "fs", "path", "process", "proxyCacheDir", "name", "path", "getCacheRoot", "safeSegment", "version", "profilePath", "dir", "binaryFingerprint", "binary", "searchPath", "candidates", "process", "candidate", "stat", "fs", "readBinaryVersion", "versionArgs", "result", "spawnSync", "reported", "readProfile", "parsed", "writeProfile", "profile", "atomicWriteFileSync", "path", "process", "NO_SESSION", "ENV_FILE_OVERRIDE_VAR", "resolveSessionId", "session", "process", "INFRA_KIT_SESSION_VAR", "resolveEnvFilePath", "override", "path", "getCacheRoot", "ENV_LOAD_FILE", "pick", "fileVars", "name", "fromFile", "fromEnv", "readListedVars", "names", "filePath", "parseVarsFromEnvFile", "vars", "value", "readUnlistedNames", "listed", "onEachLine", "stream", "visit", "buffer", "chunk", "boundary", "raw", "line", "spawn", "os", "path", "process", "goBinDir", "explicit", "process", "goPath", "path", "os", "DEFAULT_READY_TIMEOUT_MS", "DEFAULT_REQUEST_TIMEOUT_MS", "NEVER_STRIP", "PROTECTED_CHILD_ENV_NAMES", "createUpstream", "spec", "options", "binary", "args", "readyTimeoutMs", "requestTimeoutMs", "log", "onServerMessage", "searchPath", "initializeParams", "active", "starting", "generation", "failPending", "session", "reason", "deferred", "closeSession", "handleLine", "line", "message", "send", "sendRequest", "clientId", "upstreamId", "resolve", "reject", "timer", "answer", "error", "withDeadline", "promise", "deadline", "_resolve", "spawnChild", "vars", "childEnv", "name", "spawn", "startSession", "gen", "child", "onEachLine", "chunk", "code", "ensure", "key", "params", "CREDENTIAL_POLL_MS", "process", "FALLBACK_PROTOCOL_VERSION", "FALLBACK_CAPABILITIES", "log", "message", "write", "payload", "bootPortlessLink", "result", "parsed", "parseProxyArgv", "degraded", "spec", "upstream", "createUpstream", "readUnlistedNames", "readVars", "readListedVars", "versionCache", "profileCache", "binaryVersion", "readBinaryVersion", "cacheDir", "proxyCacheDir", "profile", "dir", "readProfile", "rememberProfile", "patch", "current", "next", "writeProfile", "answerInitialize", "announced", "watchForCredentials", "timer", "forwardToolsList", "vars", "response", "tools", "handshake", "answerToolsListFromCache", "handleInitialize", "resolveEnvFilePath", "answerWithoutCredentials", "handle", "onLine", "line", "error", "detail", "onEachLine"]
|
|
7
7
|
}
|
package/dist/mcp.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{a as
|
|
1
|
+
import{a as A,d as g,fa as O,ia as N,s as P}from"./chunk-7YE5QKKB.js";import{i as I}from"./chunk-YKCY657C.js";import"./chunk-KXQQUBGV.js";import{I as p,J as _,K as i,R as D,c as S}from"./chunk-63VNUKUS.js";import"./chunk-UI2MUEVR.js";import"./chunk-MFAFQVTW.js";import{a as E}from"./chunk-MFLYORUJ.js";import"./chunk-GYPVEWUV.js";import"./chunk-NFIFWIO6.js";import"./chunk-QWVLAZ6N.js";import{serveStdio as Le}from"@modelcontextprotocol/server/stdio";import m from"node:process";import{setTimeout as Ge}from"node:timers/promises";import u from"node:process";var M=e=>{u.on("uncaughtException",t=>{e.fatal({err:t,msg:"Uncaught Exception"}),e.error(`Uncaught Exception! Check ${p}. Shutting down...`),e.flush(),u.exit(1)}),u.on("unhandledRejection",(t,o)=>{e.fatal({reason:t,promise:o,msg:"Unhandled Rejection"}),e.error(`Unhandled Rejection! Check ${p}. Shutting down...`),e.flush(),u.exit(1)})};import{McpServer as $e}from"@modelcontextprotocol/server";var F='# release-create \u2014 cutting a release through infra-kit\n\nThe tool is `mcp__infra-kit__release-create`. Everything below is about calling that tool.\n\nDo not shell out. A `Bash` call running `git switch`, `git push` or `gh pr create` reproduces none\nof the preconditions below and bypasses the confirm gate in section 2 \u2014 which is the only place a\nhuman approves the release.\n\n## 1. Preconditions\n\nCheck these before the first call; each one is a refusal the human has to clear, not something to\nwork around.\n\n- **The main repository checkout, not a linked worktree.** The tool refuses outright from inside a\n linked worktree.\n- **A clean working tree.** Uncommitted changes are refused; the human commits or stashes.\n- **No other worktree holding the base branch.** Regular releases branch off `dev`, hotfixes off\n `main`. If a linked worktree has that branch checked out, the tool refuses and names the path.\n- **Jira configured.** Every release gets a matching fix version, so `JIRA_BASE_URL`,\n `JIRA_EMAIL`, `JIRA_PROJECT_ID` and `JIRA_TOKEN` (or `JIRA_API_TOKEN`) must be in the\n environment \u2014 load them with `ik env-load` and source the file it returns. The check runs before\n anything is cut.\n\n**You do not have to already be on the base branch.** The tool runs `git fetch origin`,\n`git switch <base>` and `git pull --ff-only` itself. That is a real side effect on the human\'s\ncheckout: say so before call 2.\n\n## 2. The two-call confirm protocol\n\n`release-create` is gated. **The first call never executes anything.**\n\n**Call 1** \u2014 send the real arguments, with no `confirm` and no `confirmToken`. The result is a gate\npayload, `{"status": "confirmation_required", ...}`, and it carries `"isError": true`.\n\n**That `isError` does not mean the call failed.** It is set because the payload is a gate rather\nthan the tool\'s declared output. Nothing was created, nothing was pushed, nothing was switched. An\nagent that reads it as a failure \u2014 and gives up, or retries, or falls back to `Bash` \u2014 has skipped\nthe human approval this protocol exists for. Do none of those.\n\nThe gate payload carries two things you need:\n\n- `resolvedArgs` \u2014 exactly the arguments the server bound. **Show these to the human.** This is the\n approval moment; there is no other one.\n- `confirmToken` \u2014 an HMAC bound to the tool name and to those exact arguments. It expires 600\n seconds after it is minted.\n\n**Call 2** \u2014 repeat the **same arguments**, unchanged, plus `"confirm": true` and the\n`confirmToken` from call 1.\n\nChange any argument between the two calls and round 2 comes back as\n`{"status": "confirmation_refused", "reason": "mismatch"}`. That is terminal \u2014 it is not a second\ngate. The other reasons are `absent`, `malformed`, `mac`, `expired` and `bind`, and every one of\nthem recovers the same way: call again **without** `confirm` to mint a fresh gate, then re-call\nwith those arguments and the new token. Never retry call 2 with the old token.\n\nIf the human wants different arguments, go back to call 1 with the new arguments. Do not edit the\narguments and reuse the token \u2014 that is exactly what `mismatch` refuses.\n\n## 3. What goes in `releases`\n\n`releases` is an array with at least one entry. Every entry carries **exactly one** of:\n\n- `version` \u2014 a semver string such as `"1.64.0"`, or the literal token `"next"`.\n- `name` \u2014 a free-form kebab-case identifier such as `"checkout-redesign"`.\n\nThey are mutually exclusive and one is required. An entry with both, or with neither, is rejected\nby the schema before the tool runs.\n\nEach entry also carries `type` (`"regular"` or `"hotfix"`, default `"regular"`) and an optional\n`description`, which becomes the Jira fix version\'s description and feeds the PR body.\n\n### The `"next"` token\n\n`"next"` is version-only \u2014 a named release never auto-bumps. It resolves against the union of the\nremote `release/v*` branches on `origin` and the project\'s Jira fix versions:\n\n- `"regular"` bumps the minor and resets the patch: `1.63.2` becomes `1.64.0`.\n- `"hotfix"` bumps the patch on the highest minor: `1.63.2` becomes `1.63.3`.\n\nSeveral `"next"` entries in one call advance sequentially rather than all resolving to the same\nversion.\n\n**Be honest about what `"next"` could see.** The two sources are queried in parallel and a source\nthat fails is logged and dropped, not raised \u2014 if the Jira call fails, `"next"` is computed from\nthe remote branches alone and can land on a version Jira already knows about. If neither source\nyields a prior version the tool refuses and asks for an explicit one. When the exact number\nmatters, pass the semver instead of the token.\n\n### Reading `$ARGUMENTS`\n\nThe `/infra-kit:release-create` command hands you `$ARGUMENTS` verbatim, and its argument hint is\n`[--hotfix] [--desc <text>] [<version|name>]`. **Those two flags are conventions of this command, not\nCLI flags** \u2014 `infra-kit release create` accepts neither, and the tool takes neither. They exist so a\nhuman can type the whole request on one line, and it is your job to translate them:\n\n- `--hotfix` \u2192 `type: "hotfix"` on every entry you build. Its absence means `"regular"`.\n- `--desc <text>` \u2192 `description` on the entry. The text runs to the end of the argument string.\n- The bare token \u2192 `version` when it is a semver or the literal `next`, `name` when it is kebab-case.\n\nSo `--hotfix --desc "Card expiry fix" 1.63.3` is one entry:\n`{version: "1.63.3", type: "hotfix", description: "Card expiry fix"}`.\n\nIf `$ARGUMENTS` is empty, ask the human what to cut rather than guessing a version \u2014 and read the\n`"next"` caveats above before offering it.\n\n**Precedence, when a form is also involved.** If the server answers with an argument form and the\nhuman edits it, **the form wins field by field wherever the human supplied a value, and the values\nyou parsed from `$ARGUMENTS` win everywhere else.** A human who typed `--hotfix` and then picked\n`regular` in the form gets `regular` \u2014 they saw the field and changed it. A human who typed\n`--hotfix` and left `type` untouched gets `hotfix`. Never rebuild the entry from the form alone: that\nconverts every untouched field into a silent overwrite by a value the human never saw.\n\n### Batches\n\nOne call may create several releases, but **all entries must share the same `type`**. Regular and\nhotfix branch off different bases, so a mixed batch is rejected \u2014 cut them in separate\ninvocations.\n\nA batch does not stop at the first failure. Each entry is attempted and the result reports\n`successCount`, `failureCount`, `createdBranches` and `failedReleases`. Read all four before\ntelling the human the release was created: partial success is a normal outcome here.\n\n## 4. What one release does\n\nPer entry, in order: fetch and switch to the base branch, cut `release/v<semver>` (or\n`release/<name>`), open a GitHub release PR, and create or reuse the Jira fix version (`v<semver>`\nor `<name>`).\n\nTwo consequences worth stating before call 2:\n\n- An existing fix version is **reused**, and a `description` that differs is written through to it,\n so the PR and the fix version cannot disagree.\n- A fix version that is already released or archived is **refused**, not reused. The human either\n picks a different version or un-releases it in Jira.\n\n## 5. What not to do\n\n- Do not work around a refusal with `git` or `gh`. Every refusal in section 1 is a state only the\n human can clear.\n- Do not invent a list of candidate versions for the human. Pass `"next"` and let the server\n resolve it from the real branches and fix versions, or ask for the exact semver.\n- Do not read `isError: true` on a `confirmation_required` payload as a failure. See section 2.\n';var q=`# session \u2014 switching a terminal's context through infra-kit
|
|
2
2
|
|
|
3
3
|
Three tools do the work, and this body is only the procedure that composes them:
|
|
4
4
|
\`mcp__infra-kit__env-list\`, \`mcp__infra-kit__env-load\` and \`mcp__infra-kit__env-clear\`. None of them
|
|
@@ -107,5 +107,5 @@ where nothing interposes. Raise it when a clear closely follows a load, not on e
|
|
|
107
107
|
single-quoted secrets \u2014 printing one puts it in the transcript.
|
|
108
108
|
- Do not read \`isError: true\` on a \`confirmation_required\` payload as a failure. See section 6.
|
|
109
109
|
- Do not verify a load with \`env-status\` over MCP. See section 2.
|
|
110
|
-
`;var L='# setup \u2014 bringing a machine to a working infra-kit\n\nThe tool is `mcp__infra-kit__setup`. Everything below is about calling that tool.\n\nThe same code runs behind `infra-kit setup` in a terminal, so this body describes both spellings: the\nCLI flag first, the tool field it sets second. They are one implementation, not two.\n\nDo not shell out. A `Bash` call running `brew install`, `curl \u2026 | bash` or the writers in section 1\nreproduces none of the refusals in section 3 and bypasses the confirm gate in section 4 \u2014 which is the\nonly place a human approves an install.\n\n**If all you want is to know what this machine looks like, call `doctor` instead.** It reports the same\nfive tools plus the rest of the setup, mutates nothing, and raises no confirmation prompt. `infra-kit setup` is\nthe write path; `doctor` is the read path, and they are separate tools precisely so that asking a\nquestion does not cost an approval.\n\n## 1. What one call does, in order\n\nTwo halves, and **both always run**. Neither short-circuits the other: an init-half failure is recorded\nand the dependency half still runs.\n\n### Step 1 \u2014 the init half: local, offline, additive, near-instant\n\nIn this order:\n\n1. the managed block in `~/.zshrc` \u2014 the shell integration\n2. the four config migrations, in their recorded order\n3. the user-global config seed\n4. the repo\'s agent-instruction files (`CLAUDE.md` guidance blocks) \u2014 **non-fatal**; a repo it cannot\n resolve is warned about, not failed on\n5. the git-root resolution for writes, warning when the two root gates disagree\n6. the Claude Code plugin pointer \u2014 `.claude/settings.json`, `.mcp.json`, and the plugin install\n7. the per-project config reseed\n8. a warning when `$SHELL` is not zsh\n\nEvery writer here is additive and never overwrites. Nothing in this half installs software and nothing\nreaches the network.\n\n**It runs first deliberately.** Step 1.6 is what makes the MCP surface usable at all, so it must not sit\nbehind a network converge that can be slow or fail.\n\n### Step 2 \u2014 the dependency converge\n\nFive tools, serially, in registry order: **brew, aws, gh, doppler, portless**. Serial and ordered\nbecause the recipes have prerequisites \u2014 `gh` and `doppler` both need `brew`, and doppler\'s own two\nsteps (gnupg, then the tap) must not interleave with another tool\'s.\n\nPer tool: install it when it is absent, update it when it is present, skip it when its manager is not\none infra-kit manages. A recipe the risk predicate refuses is **printed, not run** \u2014 section 3.\n\n### Step 3 \u2014 one combined summary\n\nOne line per tool, then the exact argv for every refused recipe, then the `source ~/.zshrc` reminder\nlast of all.\n\n### How to read the result\n\n- `init` \u2014 one entry per step above, each with an `outcome` of `written`, `unchanged`, `skipped` or\n `warned`, and the same message a human would have read.\n- `tools` \u2014 one entry per requested dependency: `action` (`installed`, `updated`, `skipped`, `refused`\n or `failed`), the `before` state, the `commands` that were run or would have been, and a one-line\n `detail`.\n- `converged` \u2014 whether the dependency step could act at all. `false` under `skipTools`.\n- `changed` \u2014 whether anything was installed or updated.\n- `allSucceeded` \u2014 whether no tool **failed**. **A refusal is not a failure**, so this stays `true` when\n a recipe was printed instead of run.\n\nThe process exits non-zero when either half hard-failed. **Do not read a success as "everything is\ninstalled"** \u2014 read `tools[].action`, and tell the human about every `refused` entry.\n\n## 2. The flags, and what each one narrows\n\nThe default \u2014 no flag, no field \u2014 converges all five tools.\n\n- `--tools <ids...>` \u2192 `tools: ["gh", "doppler"]`. Converge **only those ids**. Same behaviour per\n tool, smaller set. The ids are `brew`, `aws`, `gh`, `doppler` and `portless`.\n- `--update [ids...]` \u2192 `mode: "update"`. **Never installs.** A tool that is present is updated; a tool\n that is absent is reported `skipped` with the reason, and its install recipe is not run. Given ids, it\n also narrows the set, so `--update gh` is "update gh, and nothing else, and only if it is there".\n- `--skip-tools` \u2192 `skipTools: true`. A **read-only probe**. The init half still runs \u2014 it is local and\n additive \u2014 and then each tool is reported with what it needs and the exact argv that would fix it.\n Nothing is installed and nothing is updated.\n\n**`--skip-tools` with `--tools` or `--update` is a usage error, not a precedence rule.** Every\nprecedence answer is wrong: honouring `--skip-tools` would ignore a set the caller chose, and honouring\nthe other would install software the caller asked not to install. The call is refused instead.\n\n## 3. Recipes that are printed rather than run\n\nWhether a recipe may run unattended is **computed**, not a per-recipe flag someone set. Two of the four\nconjuncts are static and are applied to every recipe unconditionally:\n\n- **`needs-sudo`** \u2014 the recipe escalates privilege.\n- **`fetches-network-script`** \u2014 the recipe pipes a script fetched at run time into a shell.\n\nTwo are detection-based: **`manager-absent`** (the package manager the recipe drives is not on this\nhost) and **`manager-mismatch`** (a different manager owns the binary \u2014 running `brew upgrade` against\nan npm install, or the vendor\'s self-updater against a Homebrew keg, is the split-brain infra-kit\nrefuses for itself).\n\n**Two recipes fail the static conjuncts, and they are the two bootstraps:**\n\n- **the Homebrew bootstrap** \u2014 `/bin/bash -c "$(curl -fsSL \u2026/install.sh)"` \u2014 fails **both**: it pipes a\n network-fetched script **and** needs sudo on macOS.\n- **the first AWS CLI install** \u2014 `curl -fsSL https://awscli.amazonaws.com/v2/install.sh | bash` \u2014\n needs no sudo (it installs under `$HOME`), but it is still a network-fetched script.\n\nBoth are the tools\' own documented installers, and neither is a bug to route around. They are refused\n**by computation, applied before any detection runs**, which is what makes the refusal trustworthy: it\ncannot be widened by a probe getting something wrong, only narrowed.\n\nOnce Homebrew exists, `gh` and `doppler` install through it and run unattended; once the AWS CLI exists,\n`aws update` is a plain no-sudo recipe and runs. The refusals are a first-install cost, not permanent.\n\nWhat to do with one: the entry\'s `commands` array is the exact argv, one string per step. **Show it to\nthe human and let them run it themselves.** Do not reconstruct it as a `Bash` call \u2014 that is the same\nunattended `sudo` and the same piped script, with the control removed.\n\n## 4. The confirm gate\n\n`mcp__infra-kit__setup` is gated, and **both gates fire on every call \u2014 `skipTools` included**.\n\n**Call 1** \u2014 send the real arguments, with no `confirm` and no `confirmToken`. The result is a gate\npayload, `{"status": "confirmation_required", \u2026}`, carrying `"isError": true`.\n\n**That `isError` does not mean the call failed.** Nothing was written and nothing was installed. An\nagent that reads it as a failure \u2014 and gives up, or retries, or falls back to `Bash` \u2014 has skipped the\nhuman approval this protocol exists for. Do none of those. Show the human `resolvedArgs`; that is the\napproval moment.\n\n**Call 2** \u2014 repeat the **same arguments**, unchanged, plus `"confirm": true` and the `confirmToken`\nfrom call 1. Change any argument between the two and round 2 comes back\n`{"status": "confirmation_refused", "reason": "mismatch"}`, which is terminal, not a second gate. Every\nrefusal reason recovers the same way: call again **without** `confirm` for a fresh gate, then re-call\nwith the new token. Never retry with the old one.\n\nThe tool also carries `anthropic/requiresUserInteraction`, so the host prompts a human even where an\nallow rule would otherwise skip it. One install therefore costs two prompts. That is intended: neither\ngate substitutes for the other, and neither substitutes for the computed refusals in section 3, which\nare the only control that ships inside the CLI itself.\n\n## 5. There is no `init` command\n\nIt was removed outright. The binary rejects the name \u2014 an instruction that still says to run it fails\nat the parser rather than quietly doing something else.\n\nRepos can still say it. A consumer\'s committed CLAUDE.md block is rewritten only from inside that repo,\nso a repo upgrades the global CLI without its own text changing and can sit arbitrarily far behind. So\nwhen a human asks for "init", or a repo\'s instructions still name it:\n\n- Run **`infra-kit setup`** if they want the tools installed or updated too.\n- Run **`infra-kit setup --skip-tools`** (`skipTools: true`) for the additive local writes with nothing\n installed. That is the whole reason the flag exists: without it, removing `init` would have deleted a\n capability rather than renamed one.\n\nSay which one you chose. Running `infra-kit audit --fix` inside that repo rewrites the stale block.\n\n## 6. What not to do\n\n- Do not work around a refusal in section 3 with `Bash`. The refusal is the control.\n- Do not read `isError: true` on a `confirmation_required` payload as a failure. See section 4.\n- Do not report success from the exit status alone. Read every `tools[].action` and name the refusals.\n- Do not call this tool to answer a question. Call `doctor` \u2014 it changes nothing and prompts no one.\n';var m={"release-create":q.trimEnd(),session:j.trimEnd(),setup:L.trimEnd()};var $=async e=>{e.registerPrompt("release-create",{title:"Cut a release",description:'The procedure for cutting a release with infra-kit: preconditions, the two-call confirm protocol, and what "next" resolves against.'},()=>({messages:[{role:"user",content:{type:"text",text:m["release-create"]}}]}))};var he="infra-kit://config",ue="infra-kit://dev-context",me="infra-kit://workflow/release-create",fe="infra-kit://workflow/setup",pe="infra-kit://workflow/session",ge={loadConfig:I,readDevContext:()=>w()},y=(e,t)=>({contents:[{uri:e,mimeType:"application/json",text:JSON.stringify(t,null,2)}]}),v=(e,t)=>{e.registerResource(`infra-kit-workflow-${t.key}`,t.uri,{title:t.title,description:t.description,mimeType:"text/markdown"},o=>({contents:[{uri:o.toString(),mimeType:"text/markdown",text:m[t.key]}]}))},G=async(e,t=ge)=>{e.registerResource("infra-kit-config",he,{title:"infra-kit config",description:"The merged infra-kit.json configuration (all override layers applied). Read-only.",mimeType:"application/json"},async o=>{try{return y(o.toString(),await t.loadConfig())}catch(n){return y(o.toString(),{error:n instanceof Error?n.message:String(n)})}}),e.registerResource("infra-kit-dev-context",ue,{title:"infra-kit dev context",description:"What `infra-kit dev` last wrote: backends currently up, their ports/origins, and fragment freshness. Read-only and advisory (no liveness probe). No active session resolves to an empty payload.",mimeType:"application/json"},o=>y(o.toString(),t.readDevContext())),v(e,{key:"release-create",uri:me,title:"release-create procedure",description:'How to cut a release with the release-create tool: the preconditions, the two-call confirm protocol, and what the "next" token actually resolves against. Read this before calling mcp__infra-kit__release-create.'}),v(e,{key:"setup",uri:fe,title:"setup procedure",description:"How to set a machine up with the setup tool: the ordered local writes, then the dependency converge; what tools/mode/skipTools each narrow; which recipes are printed instead of run and why; and what to run when a repo still tells you to set it up some older way. Read this before calling mcp__infra-kit__setup."}),v(e,{key:"session",uri:pe,title:"session procedure",description:"How to switch a terminal to a named environment by composing env-list, env-load and env-clear: which tool to call when the human names no environment, how the loaded vars reach the terminal that launched Claude Code and when they appear there, and the two silent failures \u2014 a load that lands in a session nothing is watching, and a clear that loses a same-second tie to the load before it. Read this before loading an environment for someone."})};import{z as R}from"zod";import{createRequestStateCodec as we}from"@modelcontextprotocol/server";import{randomBytes as ye}from"node:crypto";var ve=new Set(["confirm","confirmToken"]),ke=600,H=e=>({toolName:e}),k=e=>{if(Array.isArray(e))return e.map(k);if(e!==null&&typeof e=="object"){let t=e;return Object.fromEntries(Object.keys(t).sort().map(o=>[o,k(t[o])]))}return e},b=e=>JSON.stringify(k(e)),c=e=>typeof e!="object"||e===null?{}:Object.fromEntries(Object.entries(e).filter(([t])=>!ve.has(t))),U=(e={})=>{let t=we({key:e.key??ye(32),ttlSeconds:e.ttlSeconds??ke,bind:o=>o.toolName});return{mint:(o,n)=>t.mint(o,H(n)),verify:(o,n)=>t.verify(o,H(n))}},W,K=()=>(W??=U(),W),be=e=>{if(typeof e!="object"||e===null)return;let{confirmToken:t}=e;return typeof t=="string"?t:void 0},xe=new Set(["malformed","mac","expired","bind"]),Ce=e=>{let t=e instanceof Error?e.message:"";return xe.has(t)?t:"malformed"},z=async(e,t,o)=>e.mint({args:b(c(o))},t),B=async(e,t,o)=>{let n=be(o);if(n===void 0)return{ok:!1,reason:"absent"};let r;try{r=await e.verify(n,t)}catch(a){return{ok:!1,reason:Ce(a)}}return r.args!==b(c(o))?{ok:!1,reason:"mismatch"}:{ok:!0}};import{acceptedContent as Te,inputRequired as J,inputResponse as Re}from"@modelcontextprotocol/server";var x="args",Z=3e3,V=e=>{let t=Re(e,x);return t.kind==="elicit"?t.action:"missing"},Se=async(e,t)=>new Promise(o=>{let n=setTimeout(()=>{o(null)},t);e.then(r=>{clearTimeout(n),o(r)},()=>{clearTimeout(n),o(null)})}),X=async(e,t,o)=>{try{return await Se(e.buildRequestedSchema(t),o)}catch{return null}},Q=async(e,t,o)=>{let n=await X(e,t,o);if(n===null)return null;try{return J({inputRequests:{[x]:J.elicit({message:e.message,requestedSchema:n})}})}catch{return null}},ee=async(e,t,o,n)=>{let r=await X(e,t,n);if(r===null)return null;let a;try{a=Te(o,x,r)}catch{return null}if(a===void 0)return null;try{return e.toArgs(a,t)}catch{return null}},Y=e=>typeof e=="object"&&e!==null&&!Array.isArray(e),te=(e,t)=>{if(!Y(e))return!1;if(!Y(t))return!0;for(let[o,n]of Object.entries(e)){if(!(o in t))return!0;let r=t[o];if(Array.isArray(n)&&(!Array.isArray(r)||r.length!==n.length))return!0}return!1};var _e=e=>typeof e=="object"&&e!==null&&e.confirm===!0,Ae=e=>e.gated?e.gated&&!e.confirmed&&e.responses===void 0&&e.canForm&&e.hasProvider&&e.formable?"form":e.gated&&!e.confirmed&&e.responses!==void 0&&!e.accepted?"declined":e.gated&&!e.confirmed&&(e.responses===void 0||e.accepted)?"gate":"verify":"run",C=e=>({content:E(JSON.stringify(e,null,2)),structuredContent:e,isError:!0}),Ee="The values you submitted in the form could NOT be applied and were DISCARDED \u2014 they failed validation or narrowed the arguments \u2014 so the arguments shown above are the ORIGINAL ones, not your selection.",De="This tool does not prompt for its arguments; you are being asked to approve the values shown above.",Ie=async(e,t,o,n)=>{let r=c(o),a=await z(e,t,o),s=[n.formDiscarded?Ee:void 0,`${t} mutates external state and is gated. It was NOT executed. Re-call ${t} with the same arguments plus "confirm": true and this "confirmToken" to execute.`,n.hasProvider?void 0:De].filter(p=>p!==void 0).join(" ");return C({status:"confirmation_required",tool:t,resolvedArgs:r,confirmToken:a,formDiscarded:n.formDiscarded,message:s})},Pe=(e,t)=>C({status:"form_declined",tool:e,action:t,message:`${e} was NOT executed: the argument form came back as "${t}". No confirmation is pending \u2014 call ${e} again to start over.`}),Oe={absent:'no "confirmToken" was supplied',malformed:'the "confirmToken" is malformed',mac:'the "confirmToken" was not issued by this server',expired:'the "confirmToken" has expired',bind:'the "confirmToken" was issued for a different tool',mismatch:'the arguments differ from the ones the "confirmToken" was issued for'},Ne=(e,t)=>C({status:"confirmation_refused",tool:e,reason:t,message:`${e} was NOT executed: ${Oe[t]}. Call ${e} again WITHOUT "confirm" to receive a fresh gate, then re-call with the same arguments plus "confirm": true and the returned "confirmToken".`}),oe=e=>{try{return e()}catch{return!1}},Me=async(e,t,o,n)=>{let r=e.formProvider;if(r===void 0||n!=="accept")return{params:t,formDiscarded:!1};let a=await ee(r,t,o,e.formDeadlineMs);return a===null?(i.info({msg:`Tool execution form discarded (validation): ${e.toolName}`}),{params:t,formDiscarded:!0}):te(c(t),a)?(i.info({msg:`Tool execution form discarded (narrowed): ${e.toolName}`}),{params:t,formDiscarded:!0}):{params:a,formDiscarded:!1}},Fe=async(e,t,o,n,r)=>{if(n==="form"&&e.formProvider!==void 0){let s=await Q(e.formProvider,t,e.formDeadlineMs);if(s!==null)return i.info({msg:`Tool execution form requested: ${e.toolName}`}),s;i.info({msg:`Tool execution form unavailable: ${e.toolName}`})}let a=await Me(e,t,o,r);return i.info({msg:`Tool execution gated (awaiting confirm): ${e.toolName}`}),await Ie(e.codec,e.toolName,a.params,{formDiscarded:a.formDiscarded,hasProvider:e.formProvider!==void 0})},qe=async(e,t,o)=>{let n=o?.mcpReq?.inputResponses,r=V(n),a=Ae({gated:e.requiresHumanConfirm===!0,confirmed:_e(t),responses:n,canForm:oe(()=>e.getClientCapabilities?.()?.elicitation?.form!==void 0),hasProvider:e.formProvider!==void 0,formable:oe(()=>e.formProvider?.isFormable(t)===!0),accepted:r==="accept"});if(a==="run")return null;if(a==="declined")return i.info({msg:`Tool execution form declined (${r}): ${e.toolName}`}),Pe(e.toolName,r);if(a==="verify"){let s=await B(e.codec,e.toolName,t);return s.ok?null:(i.info({msg:`Tool execution refused (${s.reason}): ${e.toolName}`}),Ne(e.toolName,s.reason))}return await Fe(e,t,n,a,r)},T=({toolName:e,handler:t,requiresHumanConfirm:o,formProvider:n,getClientCapabilities:r,confirmCodec:a,formDeadlineMs:s})=>{let p={toolName:e,codec:a??K(),requiresHumanConfirm:o,formProvider:n,getClientCapabilities:r,formDeadlineMs:s??Z};return async(h,S)=>{i.info({msg:`Tool execution started: ${e}`,params:h,sessionId:S?.sessionId});try{await O(),P.reset();let d=await qe(p,h,S);if(d!==null)return d;let se=await t({...h,confirmedCommand:!0});return i.info({msg:`Tool execution successful: ${e}`}),se}catch(d){throw i.error({err:d,params:h,msg:`Tool execution failed: ${e}`}),d}}};var ne=async e=>{for(let t of N())e.registerTool(t.name,{title:t.title,description:t.description,inputSchema:R.object(t.requiresHumanConfirm===!0?je(t.inputSchema):t.inputSchema),outputSchema:R.object(t.outputSchema),annotations:t.annotations,_meta:t.meta},Le(T({toolName:t.name,handler:t.handler,requiresHumanConfirm:t.requiresHumanConfirm,formProvider:t.formProvider,getClientCapabilities:()=>e.server.getClientCapabilities()})))},je=e=>({...e,confirmToken:R.string().optional().describe("Round-2 only: the token returned by the round-1 gate, proving the arguments are unchanged.")}),Le=e=>(t,o)=>e(t,o);async function re(){_.enabled=!0;let e=new $e({name:"infra-kit",version:D.version},{capabilities:{resources:{listChanged:!0},tools:{},prompts:{}}});return await $(e),await G(e),await ne(e),e}M();var l=A(),We=async()=>{try{return await re()}catch(e){l.error({err:e,msg:"Failed to create MCP server"}),l.flush(),f.exit(1)}};F(l);var Ue=Ge(We,{onerror:e=>{l.error({err:e,msg:"MCP stdio entry error"}),l.flush()}});l.info({msg:"MCP stdio entry started."});var ae=!1,ie=async e=>{if(!ae){ae=!0,l.info({msg:`Received ${e}. Shutting down...`});try{await Promise.race([Ue.close(),He(1500)])}catch(t){l.error({err:t,msg:"MCP stdio close failed during shutdown"})}l.flush(),f.exit(0)}};f.on("SIGINT",()=>{ie("SIGINT")});f.on("SIGTERM",()=>{ie("SIGTERM")});
|
|
110
|
+
`;var j='# setup \u2014 bringing a machine to a working infra-kit\n\nThe tool is `mcp__infra-kit__setup`. Everything below is about calling that tool.\n\nThe same code runs behind `infra-kit setup` in a terminal, so this body describes both spellings: the\nCLI flag first, the tool field it sets second. They are one implementation, not two.\n\nDo not shell out. A `Bash` call running `brew install`, `curl \u2026 | bash` or the writers in section 1\nreproduces none of the refusals in section 3 and bypasses the confirm gate in section 4 \u2014 which is the\nonly place a human approves an install.\n\n**If all you want is to know what this machine looks like, call `doctor` instead.** It reports the same\nfive tools plus the rest of the setup, mutates nothing, and raises no confirmation prompt. `infra-kit setup` is\nthe write path; `doctor` is the read path, and they are separate tools precisely so that asking a\nquestion does not cost an approval.\n\n## 1. What one call does, in order\n\nTwo halves, and **both always run**. Neither short-circuits the other: an init-half failure is recorded\nand the dependency half still runs.\n\n### Step 1 \u2014 the init half: local, offline, additive, near-instant\n\nIn this order:\n\n1. the managed block in `~/.zshrc` \u2014 the shell integration\n2. the four config migrations, in their recorded order\n3. the user-global config seed\n4. the repo\'s agent-instruction files (`CLAUDE.md` guidance blocks) \u2014 **non-fatal**; a repo it cannot\n resolve is warned about, not failed on\n5. the git-root resolution for writes, warning when the two root gates disagree\n6. the Claude Code plugin pointer \u2014 `.claude/settings.json`, `.mcp.json`, and the plugin install\n7. the per-project config reseed\n8. a warning when `$SHELL` is not zsh\n\nEvery writer here is additive and never overwrites. Nothing in this half installs software and nothing\nreaches the network.\n\n**It runs first deliberately.** Step 1.6 is what makes the MCP surface usable at all, so it must not sit\nbehind a network converge that can be slow or fail.\n\n### Step 2 \u2014 the dependency converge\n\nFive tools, serially, in registry order: **brew, aws, gh, doppler, portless**. Serial and ordered\nbecause the recipes have prerequisites \u2014 `gh` and `doppler` both need `brew`, and doppler\'s own two\nsteps (gnupg, then the tap) must not interleave with another tool\'s.\n\nPer tool: install it when it is absent, update it when it is present, skip it when its manager is not\none infra-kit manages. A recipe the risk predicate refuses is **printed, not run** \u2014 section 3.\n\n### Step 3 \u2014 one combined summary\n\nOne line per tool, then the exact argv for every refused recipe, then the `source ~/.zshrc` reminder\nlast of all.\n\n### How to read the result\n\n- `init` \u2014 one entry per step above, each with an `outcome` of `written`, `unchanged`, `skipped` or\n `warned`, and the same message a human would have read.\n- `tools` \u2014 one entry per requested dependency: `action` (`installed`, `updated`, `skipped`, `refused`\n or `failed`), the `before` state, the `commands` that were run or would have been, and a one-line\n `detail`.\n- `converged` \u2014 whether the dependency step could act at all. `false` under `skipTools`.\n- `changed` \u2014 whether anything was installed or updated.\n- `allSucceeded` \u2014 whether no tool **failed**. **A refusal is not a failure**, so this stays `true` when\n a recipe was printed instead of run.\n\nThe process exits non-zero when either half hard-failed. **Do not read a success as "everything is\ninstalled"** \u2014 read `tools[].action`, and tell the human about every `refused` entry.\n\n## 2. The flags, and what each one narrows\n\nThe default \u2014 no flag, no field \u2014 converges all five tools.\n\n- `--tools <ids...>` \u2192 `tools: ["gh", "doppler"]`. Converge **only those ids**. Same behaviour per\n tool, smaller set. The ids are `brew`, `aws`, `gh`, `doppler` and `portless`.\n- `--update [ids...]` \u2192 `mode: "update"`. **Never installs.** A tool that is present is updated; a tool\n that is absent is reported `skipped` with the reason, and its install recipe is not run. Given ids, it\n also narrows the set, so `--update gh` is "update gh, and nothing else, and only if it is there".\n- `--skip-tools` \u2192 `skipTools: true`. A **read-only probe**. The init half still runs \u2014 it is local and\n additive \u2014 and then each tool is reported with what it needs and the exact argv that would fix it.\n Nothing is installed and nothing is updated.\n\n**`--skip-tools` with `--tools` or `--update` is a usage error, not a precedence rule.** Every\nprecedence answer is wrong: honouring `--skip-tools` would ignore a set the caller chose, and honouring\nthe other would install software the caller asked not to install. The call is refused instead.\n\n## 3. Recipes that are printed rather than run\n\nWhether a recipe may run unattended is **computed**, not a per-recipe flag someone set. Two of the four\nconjuncts are static and are applied to every recipe unconditionally:\n\n- **`needs-sudo`** \u2014 the recipe escalates privilege.\n- **`fetches-network-script`** \u2014 the recipe pipes a script fetched at run time into a shell.\n\nTwo are detection-based: **`manager-absent`** (the package manager the recipe drives is not on this\nhost) and **`manager-mismatch`** (a different manager owns the binary \u2014 running `brew upgrade` against\nan npm install, or the vendor\'s self-updater against a Homebrew keg, is the split-brain infra-kit\nrefuses for itself).\n\n**Two recipes fail the static conjuncts, and they are the two bootstraps:**\n\n- **the Homebrew bootstrap** \u2014 `/bin/bash -c "$(curl -fsSL \u2026/install.sh)"` \u2014 fails **both**: it pipes a\n network-fetched script **and** needs sudo on macOS.\n- **the first AWS CLI install** \u2014 `curl -fsSL https://awscli.amazonaws.com/v2/install.sh | bash` \u2014\n needs no sudo (it installs under `$HOME`), but it is still a network-fetched script.\n\nBoth are the tools\' own documented installers, and neither is a bug to route around. They are refused\n**by computation, applied before any detection runs**, which is what makes the refusal trustworthy: it\ncannot be widened by a probe getting something wrong, only narrowed.\n\nOnce Homebrew exists, `gh` and `doppler` install through it and run unattended; once the AWS CLI exists,\n`aws update` is a plain no-sudo recipe and runs. The refusals are a first-install cost, not permanent.\n\nWhat to do with one: the entry\'s `commands` array is the exact argv, one string per step. **Show it to\nthe human and let them run it themselves.** Do not reconstruct it as a `Bash` call \u2014 that is the same\nunattended `sudo` and the same piped script, with the control removed.\n\n## 4. The confirm gate\n\n`mcp__infra-kit__setup` is gated, and **both gates fire on every call \u2014 `skipTools` included**.\n\n**Call 1** \u2014 send the real arguments, with no `confirm` and no `confirmToken`. The result is a gate\npayload, `{"status": "confirmation_required", \u2026}`, carrying `"isError": true`.\n\n**That `isError` does not mean the call failed.** Nothing was written and nothing was installed. An\nagent that reads it as a failure \u2014 and gives up, or retries, or falls back to `Bash` \u2014 has skipped the\nhuman approval this protocol exists for. Do none of those. Show the human `resolvedArgs`; that is the\napproval moment.\n\n**Call 2** \u2014 repeat the **same arguments**, unchanged, plus `"confirm": true` and the `confirmToken`\nfrom call 1. Change any argument between the two and round 2 comes back\n`{"status": "confirmation_refused", "reason": "mismatch"}`, which is terminal, not a second gate. Every\nrefusal reason recovers the same way: call again **without** `confirm` for a fresh gate, then re-call\nwith the new token. Never retry with the old one.\n\nThe tool also carries `anthropic/requiresUserInteraction`, so the host prompts a human even where an\nallow rule would otherwise skip it. One install therefore costs two prompts. That is intended: neither\ngate substitutes for the other, and neither substitutes for the computed refusals in section 3, which\nare the only control that ships inside the CLI itself.\n\n## 5. There is no `init` command\n\nIt was removed outright. The binary rejects the name \u2014 an instruction that still says to run it fails\nat the parser rather than quietly doing something else.\n\nRepos can still say it. A consumer\'s committed CLAUDE.md block is rewritten only from inside that repo,\nso a repo upgrades the global CLI without its own text changing and can sit arbitrarily far behind. So\nwhen a human asks for "init", or a repo\'s instructions still name it:\n\n- Run **`infra-kit setup`** if they want the tools installed or updated too.\n- Run **`infra-kit setup --skip-tools`** (`skipTools: true`) for the additive local writes with nothing\n installed. That is the whole reason the flag exists: without it, removing `init` would have deleted a\n capability rather than renamed one.\n\nSay which one you chose. Running `infra-kit audit --fix` inside that repo rewrites the stale block.\n\n## 6. What not to do\n\n- Do not work around a refusal in section 3 with `Bash`. The refusal is the control.\n- Do not read `isError: true` on a `confirmation_required` payload as a failure. See section 4.\n- Do not report success from the exit status alone. Read every `tools[].action` and name the refusals.\n- Do not call this tool to answer a question. Call `doctor` \u2014 it changes nothing and prompts no one.\n';var $={"release-create":F.trimEnd(),session:q.trimEnd(),setup:j.trimEnd()};var de="infra-kit://config",he="infra-kit://dev-context",ue="infra-kit://workflow/release-create",me="infra-kit://workflow/setup",fe="infra-kit://workflow/session",pe={loadConfig:D,readDevContext:()=>g()},w=(e,t)=>({contents:[{uri:e,mimeType:"application/json",text:JSON.stringify(t,null,2)}]}),y=(e,t)=>{e.registerResource(`infra-kit-workflow-${t.key}`,t.uri,{title:t.title,description:t.description,mimeType:"text/markdown"},o=>({contents:[{uri:o.toString(),mimeType:"text/markdown",text:$[t.key]}]}))},L=async(e,t=pe)=>{e.registerResource("infra-kit-config",de,{title:"infra-kit config",description:"The merged infra-kit.json configuration (all override layers applied). Read-only.",mimeType:"application/json"},async o=>{try{return w(o.toString(),await t.loadConfig())}catch(n){return w(o.toString(),{error:n instanceof Error?n.message:String(n)})}}),e.registerResource("infra-kit-dev-context",he,{title:"infra-kit dev context",description:"What `infra-kit dev` last wrote: backends currently up, their ports/origins, and fragment freshness. Read-only and advisory (no liveness probe). No active session resolves to an empty payload.",mimeType:"application/json"},o=>w(o.toString(),t.readDevContext())),y(e,{key:"release-create",uri:ue,title:"release-create procedure",description:'How to cut a release with the release-create tool: the preconditions, the two-call confirm protocol, and what the "next" token actually resolves against. Read this before calling mcp__infra-kit__release-create.'}),y(e,{key:"setup",uri:me,title:"setup procedure",description:"How to set a machine up with the setup tool: the ordered local writes, then the dependency converge; what tools/mode/skipTools each narrow; which recipes are printed instead of run and why; and what to run when a repo still tells you to set it up some older way. Read this before calling mcp__infra-kit__setup."}),y(e,{key:"session",uri:fe,title:"session procedure",description:"How to switch a terminal to a named environment by composing env-list, env-load and env-clear: which tool to call when the human names no environment, how the loaded vars reach the terminal that launched Claude Code and when they appear there, and the two silent failures \u2014 a load that lands in a session nothing is watching, and a clear that loses a same-second tie to the load before it. Read this before loading an environment for someone."})};import{z as x}from"zod";import{createRequestStateCodec as ge}from"@modelcontextprotocol/server";import{randomBytes as we}from"node:crypto";var ye=new Set(["confirm","confirmToken"]),ve=600,G=e=>({toolName:e}),v=e=>{if(Array.isArray(e))return e.map(v);if(e!==null&&typeof e=="object"){let t=e;return Object.fromEntries(Object.keys(t).sort().map(o=>[o,v(t[o])]))}return e},k=e=>JSON.stringify(v(e)),c=e=>typeof e!="object"||e===null?{}:Object.fromEntries(Object.entries(e).filter(([t])=>!ye.has(t))),U=(e={})=>{let t=ge({key:e.key??we(32),ttlSeconds:e.ttlSeconds??ve,bind:o=>o.toolName});return{mint:(o,n)=>t.mint(o,G(n)),verify:(o,n)=>t.verify(o,G(n))}},H,W=()=>(H??=U(),H),ke=e=>{if(typeof e!="object"||e===null)return;let{confirmToken:t}=e;return typeof t=="string"?t:void 0},be=new Set(["malformed","mac","expired","bind"]),Ce=e=>{let t=e instanceof Error?e.message:"";return be.has(t)?t:"malformed"},K=async(e,t,o)=>e.mint({args:k(c(o))},t),B=async(e,t,o)=>{let n=ke(o);if(n===void 0)return{ok:!1,reason:"absent"};let r;try{r=await e.verify(n,t)}catch(a){return{ok:!1,reason:Ce(a)}}return r.args!==k(c(o))?{ok:!1,reason:"mismatch"}:{ok:!0}};import{acceptedContent as Te,inputRequired as z,inputResponse as xe}from"@modelcontextprotocol/server";var b="args",Y=3e3,Z=e=>{let t=xe(e,b);return t.kind==="elicit"?t.action:"missing"},Re=async(e,t)=>new Promise(o=>{let n=setTimeout(()=>{o(null)},t);e.then(r=>{clearTimeout(n),o(r)},()=>{clearTimeout(n),o(null)})}),V=async(e,t,o)=>{try{return await Re(e.buildRequestedSchema(t),o)}catch{return null}},X=async(e,t,o)=>{let n=await V(e,t,o);if(n===null)return null;try{return z({inputRequests:{[b]:z.elicit({message:e.message,requestedSchema:n})}})}catch{return null}},Q=async(e,t,o,n)=>{let r=await V(e,t,n);if(r===null)return null;let a;try{a=Te(o,b,r)}catch{return null}if(a===void 0)return null;try{return e.toArgs(a,t)}catch{return null}},J=e=>typeof e=="object"&&e!==null&&!Array.isArray(e),ee=(e,t)=>{if(!J(e))return!1;if(!J(t))return!0;for(let[o,n]of Object.entries(e)){if(!(o in t))return!0;let r=t[o];if(Array.isArray(n)&&(!Array.isArray(r)||r.length!==n.length))return!0}return!1};var Se=e=>typeof e=="object"&&e!==null&&e.confirm===!0,_e=e=>e.gated?e.gated&&!e.confirmed&&e.responses===void 0&&e.canForm&&e.hasProvider&&e.formable?"form":e.gated&&!e.confirmed&&e.responses!==void 0&&!e.accepted?"declined":e.gated&&!e.confirmed&&(e.responses===void 0||e.accepted)?"gate":"verify":"run",C=e=>({content:A(JSON.stringify(e,null,2)),structuredContent:e,isError:!0}),Ae="The values you submitted in the form could NOT be applied and were DISCARDED \u2014 they failed validation or narrowed the arguments \u2014 so the arguments shown above are the ORIGINAL ones, not your selection.",Ee="This tool does not prompt for its arguments; you are being asked to approve the values shown above.",De=async(e,t,o,n)=>{let r=c(o),a=await K(e,t,o),s=[n.formDiscarded?Ae:void 0,`${t} mutates external state and is gated. It was NOT executed. Re-call ${t} with the same arguments plus "confirm": true and this "confirmToken" to execute.`,n.hasProvider?void 0:Ee].filter(f=>f!==void 0).join(" ");return C({status:"confirmation_required",tool:t,resolvedArgs:r,confirmToken:a,formDiscarded:n.formDiscarded,message:s})},Ie=(e,t)=>C({status:"form_declined",tool:e,action:t,message:`${e} was NOT executed: the argument form came back as "${t}". No confirmation is pending \u2014 call ${e} again to start over.`}),Pe={absent:'no "confirmToken" was supplied',malformed:'the "confirmToken" is malformed',mac:'the "confirmToken" was not issued by this server',expired:'the "confirmToken" has expired',bind:'the "confirmToken" was issued for a different tool',mismatch:'the arguments differ from the ones the "confirmToken" was issued for'},Oe=(e,t)=>C({status:"confirmation_refused",tool:e,reason:t,message:`${e} was NOT executed: ${Pe[t]}. Call ${e} again WITHOUT "confirm" to receive a fresh gate, then re-call with the same arguments plus "confirm": true and the returned "confirmToken".`}),te=e=>{try{return e()}catch{return!1}},Ne=async(e,t,o,n)=>{let r=e.formProvider;if(r===void 0||n!=="accept")return{params:t,formDiscarded:!1};let a=await Q(r,t,o,e.formDeadlineMs);return a===null?(i.info({msg:`Tool execution form discarded (validation): ${e.toolName}`}),{params:t,formDiscarded:!0}):ee(c(t),a)?(i.info({msg:`Tool execution form discarded (narrowed): ${e.toolName}`}),{params:t,formDiscarded:!0}):{params:a,formDiscarded:!1}},Me=async(e,t,o,n,r)=>{if(n==="form"&&e.formProvider!==void 0){let s=await X(e.formProvider,t,e.formDeadlineMs);if(s!==null)return i.info({msg:`Tool execution form requested: ${e.toolName}`}),s;i.info({msg:`Tool execution form unavailable: ${e.toolName}`})}let a=await Ne(e,t,o,r);return i.info({msg:`Tool execution gated (awaiting confirm): ${e.toolName}`}),await De(e.codec,e.toolName,a.params,{formDiscarded:a.formDiscarded,hasProvider:e.formProvider!==void 0})},Fe=async(e,t,o)=>{let n=o?.mcpReq?.inputResponses,r=Z(n),a=_e({gated:e.requiresHumanConfirm===!0,confirmed:Se(t),responses:n,canForm:te(()=>e.getClientCapabilities?.()?.elicitation?.form!==void 0),hasProvider:e.formProvider!==void 0,formable:te(()=>e.formProvider?.isFormable(t)===!0),accepted:r==="accept"});if(a==="run")return null;if(a==="declined")return i.info({msg:`Tool execution form declined (${r}): ${e.toolName}`}),Ie(e.toolName,r);if(a==="verify"){let s=await B(e.codec,e.toolName,t);return s.ok?null:(i.info({msg:`Tool execution refused (${s.reason}): ${e.toolName}`}),Oe(e.toolName,s.reason))}return await Me(e,t,n,a,r)},T=({toolName:e,handler:t,requiresHumanConfirm:o,formProvider:n,getClientCapabilities:r,confirmCodec:a,formDeadlineMs:s})=>{let f={toolName:e,codec:a??W(),requiresHumanConfirm:o,formProvider:n,getClientCapabilities:r,formDeadlineMs:s??Y};return async(h,R)=>{i.info({msg:`Tool execution started: ${e}`,params:h,sessionId:R?.sessionId});try{await P(),I.reset();let d=await Fe(f,h,R);if(d!==null)return d;let ie=await t({...h,confirmedCommand:!0});return i.info({msg:`Tool execution successful: ${e}`}),ie}catch(d){throw i.error({err:d,params:h,msg:`Tool execution failed: ${e}`}),d}}};var oe=async e=>{for(let t of O())e.registerTool(t.name,{title:t.title,description:t.description,inputSchema:x.object(t.requiresHumanConfirm===!0?qe(t.inputSchema):t.inputSchema),outputSchema:x.object(t.outputSchema),annotations:t.annotations,_meta:t.meta},je(T({toolName:t.name,handler:t.handler,requiresHumanConfirm:t.requiresHumanConfirm,formProvider:t.formProvider,getClientCapabilities:()=>e.server.getClientCapabilities()})))},qe=e=>({...e,confirmToken:x.string().optional().describe("Round-2 only: the token returned by the round-1 gate, proving the arguments are unchanged.")}),je=e=>(t,o)=>e(t,o);async function ne(){S.enabled=!0;let e=new $e({name:"infra-kit",version:E.version},{capabilities:{resources:{listChanged:!0},tools:{}}});return await L(e),await oe(e),e}N();var l=_(),He=async()=>{try{return await ne()}catch(e){l.error({err:e,msg:"Failed to create MCP server"}),l.flush(),m.exit(1)}};M(l);var Ue=Le(He,{onerror:e=>{l.error({err:e,msg:"MCP stdio entry error"}),l.flush()}});l.info({msg:"MCP stdio entry started."});var re=!1,ae=async e=>{if(!re){re=!0,l.info({msg:`Received ${e}. Shutting down...`});try{await Promise.race([Ue.close(),Ge(1500)])}catch(t){l.error({err:t,msg:"MCP stdio close failed during shutdown"})}l.flush(),m.exit(0)}};m.on("SIGINT",()=>{ae("SIGINT")});m.on("SIGTERM",()=>{ae("SIGTERM")});
|
|
111
111
|
//# sourceMappingURL=mcp.js.map
|