@pinta-ai/mcp-logger 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
function e(...e){console.error("[mcp-logger]",...e)}function t(...e){N&&console.error("[mcp-logger]",...e)}function s(...e){b&&console.error("[mcp-logger]",...e)}function r(e){return!k.includes(e)}function n(e){try{const t=new URL(e),s=t.pathname.replace(/\/+$/,"");return`${t.host}${s}`}catch{return e}}function i(e,t){return{decision:"ALLOW",reason:null,durationMs:Date.now()-e,failOpenReason:t}}function o(e){const t=[];for(const[s,r]of Object.entries(e))null!=r&&("string"==typeof r?t.push({key:s,value:{stringValue:r}}):"boolean"==typeof r?t.push({key:s,value:{boolValue:r}}):Number.isInteger(r)?t.push({key:s,value:{intValue:r}}):t.push({key:s,value:{doubleValue:r}}));return t}function a(e){if(/^[0-9A-HJKMNP-TV-Z]{26}$/i.test(e)){let t=0n;for(const s of e.toUpperCase())t=t<<5n|BigInt("0123456789ABCDEFGHJKMNPQRSTVWXYZ".indexOf(s));return t&=(1n<<128n)-1n,t.toString(16).padStart(32,"0")}return A("sha256").update(e).digest("hex").slice(0,32)}function p(e,t){return A("sha256").update(`${e}|${t+""}`).digest("hex").slice(0,16)}function c(e){let t="";for(let s=0;e>s;s++)t+=Math.floor(256*Math.random()).toString(16).padStart(2,"0");return t}function u(e,t=2e5){try{const s=JSON.stringify(e);return s.length>t?s.slice(0,t)+"... (truncated)":s}catch{return null}}function d(e,t,s,r,n){const i=function(e,t){const s=e?.getClientInfo();if(s?.name||s?.version)return{name:s.name,version:s.version};const r=e?.getHeaders()||t;if(r){const e=r["user-agent"]||r["User-Agent"];if(e){const t=e.match(/^([^\/]+)\/([^\s]+)/);if(t)return{name:t[1],version:t[2]}}}return{name:void 0,version:void 0}}(r,n),o=r?function(e){let t=J.get(e);return t||(t=new Z,J.set(e,t)),t}(r):B,a=`${i.name||"unknown"}-${i.version||"0"}`,p=o.getOrCreateSessionId(r?"connection":a),c=o.getOrCreateConversationId(p,e),u=function(e,t){return e?.userId||e?.user_id?e.userId||e.user_id:t?t["x-user-id"]||t["X-User-Id"]:void 0}(t,n),d=function(e){if(e?.permission||e?.permissionLevel)return e.permission||e.permissionLevel}(t),h=function(e,t){return void 0!==e?.cost?e.cost:void 0!==t?.cost?t.cost:void 0}(t,s),l={};return p&&(l.sessionId=p),c&&(l.conversationId=c),u&&(l.userId=u),i.name&&(l.clientName=i.name),i.version&&(l.clientVersion=i.version),d&&(l.permissionLevel=d),void 0!==h&&(l.cost=h),l}function h(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}function l(e){if("string"!=typeof e)return;const t=e.trim();return!t||"unknown"===t.toLowerCase()||t.startsWith("{")||t.startsWith("[")?void 0:t}function m(e){if(h(e))return l(e["sh.pinta/model-provider"])}function f(e,t,s){if(null==e.id)return;if("response"===t&&"sampling/createMessage"===e.method){if(s?.id!==e.id||void 0!==s.error||!h(s.result))return;const t=l(s.result.model);if(void 0===t)return;return{model:t,source:"sampling_response",provider:m(s.result._meta)}}if("request"!==t||"tools/call"!==e.method||!h(e.params))return;const r=e.params._meta;if(!h(r))return;const n=l(r["sh.pinta/model"]);return void 0!==n?{model:n,source:"pinta_request_meta",provider:m(r)}:void 0}function v(e,t={}){if(0===e.length)return e;const s=function(e,t){const s=[];for(const r of j){if(r.requireContext&&r.requireContext!==t.context)continue;const n=RegExp(r.regex.source,r.regex.flags);let i;for(;null!==(i=n.exec(e));){const e=i[r.captureGroup??0];if(void 0===e){i.index===n.lastIndex&&n.lastIndex++;continue}const t=i.index,o=i.index+i[0].length,a=t+i[0].indexOf(e),p=a+e.length;s.push({start:t,end:o,replaceStart:a,replaceEnd:p,type:r.type}),i.index===n.lastIndex&&n.lastIndex++}}return s}(e,t);return 0===s.length?e:function(e,t){const s=[...t].sort((e,t)=>t.replaceStart-e.replaceStart);let r=e;for(const e of s)r=r.slice(0,e.replaceStart)+`[PINTA_REDACTED:${e.type}]`+r.slice(e.replaceEnd);return r}(e,function(e){const t=[...e].sort((e,t)=>e.start!==t.start?e.start-t.start:t.end-t.start-(e.end-e.start)),s=[];let r=-1;for(const e of t)e.start<r||(s.push(e),r=e.end);return s}(s))}function g(e){W.build(e)}import{program as y}from"commander";import{NodeSDK as I}from"@opentelemetry/sdk-node";import{OTLPTraceExporter as x}from"@opentelemetry/exporter-trace-otlp-http";import{Resource as S}from"@opentelemetry/resources";import{trace as w,SpanStatusCode as D,context as E,TraceFlags as _}from"@opentelemetry/api";import{execSync as q,spawn as M}from"child_process";import{Transform as T}from"stream";import{createHash as A}from"node:crypto";import{ulid as R}from"ulid";const C="0.3.1",N="true"===process.env.PINTA_DEBUG||"1"===process.env.PINTA_DEBUG,b="true"===process.env.PINTA_RAW_DEBUG||"1"===process.env.PINTA_RAW_DEBUG,O=C,k=["notifications/initialized"],P=process.env.PINTA_GUARD_ENDPOINT;!function(){if(process.env.PINTA_RELAY_TOKEN)return;const e=(process.env.OTEL_EXPORTER_OTLP_HEADERS??"").match(/x-pinta-relay-token=([^,\s]+)/i);e&&(process.env.PINTA_RELAY_TOKEN=e[1])}();const L=new class{sdk=null;tracer=null;initialized=!1;cliServerInfo=null;resourceAttrs={"telemetry.sdk.name":"mcp-logger","telemetry.sdk.version":O};initialize(e){if(this.initialized)return t("Telemetry already initialized, returning existing tracer"),this.tracer;const s=e.serviceName,r=e.serviceVersion||"unknown";t("Initializing telemetry..."),t("Service: "+s),t("Version: "+r);const n={...this.resourceAttrs,"service.name":s,"service.version":r};this.resourceAttrs=n,this.sdk=new I({resource:new S(n),traceExporter:new x}),this.sdk.start(),this.tracer=w.getTracer("mcp-logger",O),this.initialized=!0,t("Telemetry initialized successfully");const i=async()=>{t("Shutting down telemetry..."),await this.shutdown()};return process.once("SIGTERM",i),process.once("SIGINT",i),this.tracer}getTracer(){return this.tracer}isInitialized(){return this.initialized}getResourceAttributes(){return{...this.resourceAttrs}}setCLIServerInfo(e,t,s){this.cliServerInfo={command:e,args:t,env:s}}getCLIServerInfo(){return this.cliServerInfo}async shutdown(){if(this.sdk)try{await this.sdk.shutdown(),this.sdk=null,this.tracer=null,this.initialized=!1,this.cliServerInfo=null}catch(e){console.error("[mcp-logger] Error shutting down telemetry:",e)}}},U=new Set(["mcp-remote","supergateway","mcp-proxy"]),$=new Set(["npx","node","uvx","python","python3","pip","pipx","deno","bun"]),G=new Set(["uvx","python","python3","pip","pipx"]),z="mcp-logger/"+C;class Z{sessionMap=new Map;sessionCounter=0;conversationMap=new Map;conversationCounter=0;getOrCreateSessionId(e){if(!this.sessionMap.has(e)){const t=R();this.sessionMap.set(e,t),this.sessionCounter++}return this.sessionMap.get(e)}getOrCreateConversationId(e,t){const s=`${e}-${t}`;return this.conversationMap.has(s)||this.conversationMap.set(s,"conv-"+ ++this.conversationCounter),this.conversationMap.get(s)}clear(){this.sessionMap.clear(),this.conversationMap.clear(),this.sessionCounter=0,this.conversationCounter=0}}const B=new Z;let J=new WeakMap;const j=[{type:"aws_access_key",regex:/AKIA[0-9A-Z]{16}/g},{type:"aws_secret_key",regex:/(?:aws[_-]?secret(?:[_-]?(?:access)?[_-]?key)?)\s*[:=]\s*["']?([A-Za-z0-9/+=]{40})(?![A-Za-z0-9/+=])/gi,captureGroup:1},{type:"gcp_service_account",regex:/\{[\s\S]{0,200}?"type"\s*:\s*"service_account"[\s\S]*?\}/g},{type:"github_token",regex:/gh[pousr]_[A-Za-z0-9]{36,}/g},{type:"gitlab_token",regex:/glpat-[A-Za-z0-9_-]{20}/g},{type:"slack_token",regex:/xox[abrsp]-[0-9A-Za-z-]{10,}/g},{type:"openai_key",regex:/sk-(?:proj-)?[A-Za-z0-9_-]{40,}/g},{type:"anthropic_key",regex:/sk-ant-[A-Za-z0-9_-]{50,}/g},{type:"stripe_key",regex:/(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{20,}/g},{type:"jwt",regex:/eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g},{type:"private_key_block",regex:/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g},{type:"bearer_token",regex:/bearer\s+([A-Za-z0-9._~+/=-]{12,})/gi,captureGroup:1},{type:"basic_auth",regex:/basic\s+([A-Za-z0-9+/=]{12,})/gi,captureGroup:1},{type:"db_url_password",regex:/\b(?:postgres|postgresql|mysql|mariadb|mongodb(?:\+srv)?|redis):\/\/[^:\s/]+:([^@\s]+)@/gi,captureGroup:1},{type:"cli_password_flag",regex:/(?:--password|--pass|--pwd)[=\s]([^\s'"]+)/g,captureGroup:1},{type:"cli_password_short",regex:/\s-p([^\s'"]+)/g,captureGroup:1,requireContext:"bash"},{type:"env_var_secret",regex:/^(?:export\s+)?([A-Z][A-Z0-9_]*(?:KEY|SECRET|TOKEN|PASSWORD|PASSWD|PWD|API_KEY)[A-Z0-9_]*)\s*=\s*["']?([^\s"'\n]+)/gm,captureGroup:2}];class V{spanData;input;contextMetadata;buildPayload(e){const t=this.buildAttributes(e),s=this.contextMetadata.sessionId;return function(e){const t=e.now??Date.now(),s=""+1000000n*BigInt(t),r=[];for(const[t,s]of Object.entries(e.resource))void 0!==s&&r.push({key:t,value:{stringValue:s}});const n={traceId:e.traceId??c(16),spanId:e.spanId??c(8),...e.parentSpanId?{parentSpanId:e.parentSpanId}:{},name:e.name,kind:1,startTimeUnixNano:s,endTimeUnixNano:s,attributes:o(e.attributes)};return{resourceSpans:[{resource:{attributes:r},scopeSpans:[{scope:e.scope,spans:[n]}]}]}}({name:"mcp."+e.method,attributes:t,resource:L.getResourceAttributes(),scope:{name:"mcp-logger",version:C},...s?{traceId:a(s)}:{},...s&&void 0!==e.requestId?{spanId:p(s,this.exchangeId())}:{},now:e.requestTimestamp})}buildAttributes(e){if(this.input=e,this.spanData=this.buildBase(),this.resolveContextMetadata(),this.applyRequestFields(),this.applyResponseFields(),this.applyDuration(),this.applyCLIServerInfo(),this.applyMCPServerInfo(),this.applyContextMetadata(),this.applyGuard(),this.applyModelEvidence(),this.input.customEvents&&this.input.customEvents.length>0&&(this.spanData["mcp.events"]=JSON.stringify(this.input.customEvents),this.spanData["mcp.events.count"]=this.input.customEvents.length),e.error){this.spanData["mcp.error"]=!0;const t="string"==typeof e.error?e.error:e.error instanceof Error?e.error.message:u(e.error)||"Unknown error";this.spanData["mcp.error.message"]=t}return{...this.spanData}}build(e){const t=L.getTracer();if(!t)return;this.input=e,this.resolveContextMetadata();const r=this.parentContext();t.startActiveSpan("mcp."+e.method,{},r,e=>{try{this.spanData=this.buildBase(),this.applyRequestFields(),this.applyResponseFields(),this.applyDuration(),this.applyCLIServerInfo(),this.applyMCPServerInfo(),this.applyContextMetadata(),this.applyGuard(),this.applyModelEvidence(),this.applyCustomEvents(e),this.applyError(e),function(e,t){for(const[s,r]of Object.entries(t))null!=r&&e.setAttribute(s,r)}(e,this.spanData),s("[OTLP] Span data:",JSON.stringify(this.spanData,null,2))}catch{e.setStatus({code:D.ERROR,message:"Error creating span"})}finally{e.end()}})}buildBase(){const e={"ingest.type":"mcp","mcp.type":this.input.type,"mcp.transport":this.input.transport,"mcp.method":this.input.method};this.input.serverUrl&&(e["mcp.server.url"]=this.input.serverUrl);const t=function(e=process.env){const t=e.PINTA_MCP_CONFIG_KEY;if("string"!=typeof t)return;const s=t.trim();return s.length>0?s:void 0}();return t&&(e["mcp.server.config_key"]=t),e}parentContext(){const e=this.contextMetadata.sessionId;if(!e)return E.active();const t=this.exchangeId();return w.setSpanContext(E.active(),{traceId:a(e),spanId:p(e,void 0===t?"session":t),traceFlags:_.SAMPLED,isRemote:!0})}exchangeId(){const e=this.input.requestId??this.input.responseId;return void 0!==e&&this.input.requestDirection?`${this.input.requestDirection}:${typeof e}:${e}`:e}resolveContextMetadata(){this.contextMetadata=this.input.contextMetadata||d(this.input.method,this.input.params,this.input.result,void 0,this.input.headers)}applyRequestFields(){if("response"!==this.input.type){if("paired"===this.input.type||"request"===this.input.type||"notification"===this.input.type){if(void 0!==this.input.requestId&&(this.spanData["mcp.request.id"]=this.input.requestId+""),this.spanData["mcp.request.method"]=this.input.method,void 0!==this.input.requestTimestamp&&(this.spanData["mcp.request.timestamp"]=this.input.requestTimestamp),void 0!==this.input.params){const e=u(this.input.params)||"{}";this.spanData["mcp.request.params"]=v(e,{context:"bash"})}if(this.input.headers){const e=u(this.input.headers);e&&(this.spanData["mcp.headers"]=e)}}}else void 0!==this.input.requestId&&(this.spanData["mcp.request.id"]=this.input.requestId+"")}applyResponseFields(){if(("paired"===this.input.type||"response"===this.input.type)&&(void 0!==this.input.responseId&&(this.spanData["mcp.response.id"]=this.input.responseId+""),this.spanData["mcp.response.method"]=this.input.method,void 0!==this.input.responseTimestamp&&(this.spanData["mcp.response.timestamp"]=this.input.responseTimestamp),void 0!==this.input.result)){const e=u(this.input.result);e&&(this.spanData["mcp.response.result"]=v(e,{context:"bash"}))}}applyDuration(){void 0!==this.input.durationMs&&(this.spanData["mcp.duration_ms"]=this.input.durationMs)}applyCLIServerInfo(){const e=L.getCLIServerInfo();e&&(this.spanData["mcp.cli"]=JSON.stringify({command:e.command,args:e.args,env:e.env}))}applyMCPServerInfo(){this.input.serverInfo&&(this.input.serverInfo.name&&(this.spanData["mcp.sdk.name"]=this.input.serverInfo.name),this.input.serverInfo.version&&(this.spanData["mcp.sdk.version"]=this.input.serverInfo.version))}applyContextMetadata(){this.contextMetadata.sessionId&&(this.spanData["mcp.session.id"]=this.contextMetadata.sessionId),this.contextMetadata.conversationId&&(this.spanData["mcp.conversation.id"]=this.contextMetadata.conversationId),this.contextMetadata.userId&&(this.spanData["mcp.user.id"]=this.contextMetadata.userId),this.contextMetadata.clientName&&(this.spanData["mcp.client.name"]=this.contextMetadata.clientName),this.contextMetadata.clientVersion&&(this.spanData["mcp.client.version"]=this.contextMetadata.clientVersion),this.contextMetadata.permissionLevel&&(this.spanData["mcp.permission.level"]=this.contextMetadata.permissionLevel),void 0!==this.contextMetadata.cost&&(this.spanData["mcp.cost"]=this.contextMetadata.cost)}applyGuard(){const e=this.input.guard;e&&(this.spanData["pinta.guard.decision"]=e.decision.toLowerCase(),this.spanData["pinta.guard.duration_ms"]=Math.round(e.durationMs),e.reason&&(this.spanData["pinta.guard.matched_rule"]=e.reason),e.failOpenReason&&(this.spanData["pinta.guard.fail_open_reason"]=e.failOpenReason))}applyModelEvidence(){const e=this.input.modelEvidence,t=l(e?.model);if(!e||void 0===t)return;this.spanData["mcp.model"]=t,this.spanData["mcp.model_source"]=e.source;const s=l(e.provider);void 0!==s&&(this.spanData["mcp.model_provider"]=s)}applyCustomEvents(e){this.input.customEvents&&0!==this.input.customEvents.length&&(this.spanData["mcp.events"]=JSON.stringify(this.input.customEvents),this.spanData["mcp.events.count"]=this.input.customEvents.length,this.input.customEvents.forEach(t=>{e.addEvent("custom."+(t.level||"info"),{message:t.message,metadata:JSON.stringify(t.metadata||{}),timestamp:t.timestamp||Date.now()})}))}applyError(e){if(this.input.error){let t;this.spanData["mcp.error"]=!0,this.input.error instanceof Error?(t=this.input.error.message,e.recordException(this.input.error)):t="string"==typeof this.input.error?this.input.error:u(this.input.error)||"Unknown error",this.spanData["mcp.error.message"]=t,e.setStatus({code:D.ERROR,message:t})}else e.setStatus({code:D.OK})}}const W=new V,F=new Set(["tools/call","resources/read","prompts/get"]);class K extends T{buffer="";direction;logger;sessionContextStore;constructor(e,t,s){super(),this.direction=e,this.logger=t,this.sessionContextStore=s}_transform(e,s,r){try{if("request"===this.direction){this.buffer+=e.toString();const t=this.buffer.split("\n");return this.buffer=t.pop()||"",void(async()=>{for(const e of t)await this.processRequestLine(e)})().then(()=>r()).catch(e=>r(e))}this.buffer+=e.toString(),this.buffer.length>1e6&&(t("Buffer size exceeded, clearing..."),this.buffer=""),this.processBuffer(!0),r()}catch(e){r(e)}}processBuffer(e){const r=this.buffer.split("\n");this.buffer=r.pop()||"";for(const n of r){const r=n.trim();if(r)try{const n=JSON.parse(r);if(e){if("2.0"!==n.jsonrpc){t("Non-JSON-RPC JSON ignored:",r.slice(0,100));continue}this.push(r+"\n")}s(`[${this.direction}] Raw message:`,r),this.handleMessage(n)}catch{t(e?"Non-JSON output filtered:":"Failed to parse:",r.slice(0,100))}}}handleMessage(e){e.method?this.logger.onRequest(e,void 0,this.direction):null!=e.id&&this.logger.onResponse(e,this.direction)}async processRequestLine(e,r="\n"){const n=e.trim();if(!n)return;let i;try{i=JSON.parse(n)}catch{return t("Failed to parse:",n.slice(0,100)),void this.push(e+r)}if(i?.method&&F.has(i.method)){const e=i,t=await this.evaluateRequestGuard(e);if("DENY"===t?.decision){const r={jsonrpc:"2.0",id:e.id??null,error:{code:-32e3,message:"Pinta guard denied: "+(t.reason??"guard_deny"),data:{reason:t.reason}}};return process.stdout.write(JSON.stringify(r)+"\n"),s(`[${this.direction}] DENY:`,n),void this.logger.onRequestDenied(e,t)}this.logger.onRequest(e,t??void 0)}else i&&this.handleMessage(i);this.push(e+r),s(`[${this.direction}] Raw message:`,n)}async evaluateRequestGuard(e){const t=process.env.PINTA_GUARD_ENDPOINT??P;return t&&"1"!==process.env.PINTA_GUARD_DISABLED?async function(e,t){if(!t)return null;if("1"===process.env.PINTA_GUARD_DISABLED)return null;const s=Date.now(),r=new AbortController,n=setTimeout(()=>r.abort(),50);try{const n=await fetch(t,{method:"POST",headers:{"content-type":"application/json","user-agent":z,"x-pinta-relay-token":process.env.PINTA_RELAY_TOKEN??""},body:JSON.stringify(e),signal:r.signal});if(410===n.status)return i(s,"refused");if(200!==n.status)return i(s,"error");const o=await n.json();return!o||"ALLOW"!==o.decision&&"DENY"!==o.decision&&"REVIEW"!==o.decision?i(s,"error"):{decision:o.decision,reason:o.reason??null,durationMs:o.durationMs??Date.now()-s}}catch(e){const t=e.name;return i(s,"AbortError"===t||"TimeoutError"===t?"timeout":"error")}finally{clearTimeout(n)}}((new V).buildPayload({type:"request",method:e.method,transport:this.logger.transport,serverUrl:this.logger.serverUrl,requestId:e.id??void 0,requestDirection:"request",requestTimestamp:Date.now(),params:e.params,serverInfo:this.sessionContextStore.getServerInfo(),contextMetadata:d(e.method,e.params,void 0,this.sessionContextStore),modelEvidence:f(e,"request")}),t):null}_flush(e){const s=()=>{N&&t("Stream closed. Pending requests: "+this.logger.getStats().pending),e()};try{if("request"===this.direction){const t=this.buffer;return this.buffer="",void this.processRequestLine(t,"").then(s,t=>e(t))}if(this.buffer.trim())try{const e=JSON.parse(this.buffer);"response"===this.direction&&"2.0"===e.jsonrpc&&this.push(this.buffer.trim()+"\n"),this.handleMessage(e)}catch{t("Failed to parse final buffer")}s()}catch(t){e(t)}}}class Y{pendingRequests={request:new Map,response:new Map};TIMEOUT=3e4;sessionContextStore;transport;serverUrl;constructor(e,t={}){this.sessionContextStore=e,this.transport=t.transport??"stdio",this.serverUrl=t.serverUrl}onRequest(e,t,s="request"){if(!r(e.method))return;if(null==e.id)return void this.onNotification(e);"request"===s&&"initialize"===e.method&&e.params?.clientInfo&&this.sessionContextStore.setClientInfo({name:e.params.clientInfo.name,version:e.params.clientInfo.version});const n=this.pendingRequests[s],i=n.get(e.id);i&&(clearTimeout(i.timeout),clearTimeout(i.sweep));const o={request:e,timestamp:Date.now(),guard:t,direction:s,timeout:setTimeout(()=>this.handleTimeout(e.id,s),this.TIMEOUT)};o.timeout.unref?.(),n.set(e.id,o)}onRequestDenied(e,t){if(!r(e.method))return;const s=d(e.method,e.params,void 0,this.sessionContextStore,void 0);g({type:"request",method:e.method,transport:this.transport,serverUrl:this.serverUrl,requestId:e.id??void 0,requestDirection:"request",requestTimestamp:Date.now(),params:e.params,contextMetadata:s,guard:t,modelEvidence:f(e,"request")})}onResponse(e,t="response"){if(null==e.id)return;const s="response"===t?"request":"response",r=this.pendingRequests[s],n=r.get(e.id),i=Date.now(),o=n?.request.method||e.method||"unknown";let a=this.sessionContextStore.getServerInfo();"request"===s&&"initialize"===n?.request.method&&e.result?.serverInfo&&(a={name:e.result.serverInfo.name,version:e.result.serverInfo.version},this.sessionContextStore.setServerInfo(a));const p=d(o,n?.request.params,e.result,this.sessionContextStore,void 0);n?(clearTimeout(n.timeout),clearTimeout(n.sweep),r.delete(e.id),g({type:n.timedOut?"response":"paired",method:o,transport:this.transport,serverUrl:this.serverUrl,requestId:n.request.id,requestDirection:s,requestTimestamp:n.timedOut?void 0:n.timestamp,params:n.timedOut?void 0:n.request.params,responseId:e.id,responseTimestamp:i,result:e.result,error:e.error,durationMs:i-n.timestamp,contextMetadata:p,serverInfo:a,guard:n.guard,modelEvidence:f(n.request,s,e)})):g({type:"response",method:o,transport:this.transport,serverUrl:this.serverUrl,responseId:e.id,requestDirection:s,responseTimestamp:i,result:e.result,error:e.error,contextMetadata:p,serverInfo:a})}onNotification(e){if(!r(e.method))return;const t=d(e.method,e.params,void 0,this.sessionContextStore,void 0);g({type:"notification",method:e.method,transport:this.transport,serverUrl:this.serverUrl,params:e.params,contextMetadata:t})}handleTimeout(e,t){const s=this.pendingRequests[t],r=s.get(e);r&&!r.timedOut&&(this.emitRequestSpan(r),r.timedOut=!0,r.sweep=setTimeout(()=>{s.get(e)===r&&s.delete(e)},6e5),r.sweep.unref?.())}emitRequestSpan(e){const t=d(e.request.method,e.request.params,void 0,this.sessionContextStore,void 0);g({type:"request",method:e.request.method,transport:this.transport,serverUrl:this.serverUrl,requestId:e.request.id,requestDirection:e.direction,requestTimestamp:e.timestamp,params:e.request.params,contextMetadata:t,guard:e.guard,modelEvidence:f(e.request,e.direction)})}flushPending(){for(const e of Object.values(this.pendingRequests))for(const t of e.values())t.timedOut||this.emitRequestSpan(t);this.clear()}getStats(){return{pending:this.pendingRequests.request.size+this.pendingRequests.response.size}}clear(){for(const e of Object.values(this.pendingRequests)){for(const t of e.values())clearTimeout(t.timeout),clearTimeout(t.sweep);e.clear()}}}class H{context={};getClientInfo(){return this.context.clientInfo}setClientInfo(e){this.context.clientInfo=e}getServerInfo(){return this.context.serverInfo}setServerInfo(e){this.context.serverInfo=e}getHeaders(){return this.context.headers}setHeaders(e){this.context.headers=e}snapshot(){return{clientInfo:this.context.clientInfo?{...this.context.clientInfo}:void 0,serverInfo:this.context.serverInfo?{...this.context.serverInfo}:void 0,headers:this.context.headers?{...this.context.headers}:void 0}}}class X{process;logger;isShuttingDown=!1;constructor(e,t,s){const r=new H;this.logger=new Y(r,{transport:s.transport,serverUrl:s.url}),this.process=M(e,t,{stdio:["pipe","pipe","inherit"],env:{...process.env,MCP_LOGGER_ENABLED:"true"}});const n=new K("request",this.logger,r),i=new K("response",this.logger,r);process.stdin.pipe(n).pipe(this.process.stdin),this.process.stdout.pipe(i).pipe(process.stdout),this.process.on("error",e=>this.handleError(e)),this.process.on("exit",(e,t)=>this.handleExit(e,t)),process.on("SIGTERM",()=>this.shutdown("SIGTERM")),process.on("SIGINT",()=>this.shutdown("SIGINT"))}async handleError(t){e("Failed to start MCP server:",t),await this.cleanup(),process.exit(1)}async handleExit(e,s){t(`MCP server exited with code ${e}, signal ${s}`),await this.cleanup(),process.exit(e||0)}async shutdown(e){this.isShuttingDown||(this.isShuttingDown=!0,t(`Received ${e}, shutting down...`),this.process.kill(e),await Promise.race([new Promise(e=>this.process.once("exit",e)),new Promise(e=>setTimeout(e,5e3))]),await this.cleanup(),process.exit(0))}async cleanup(){this.logger.flushPending(),await async function(){await L.shutdown()}()}}y.name("mcp-logger").description("Wrap an MCP server and forward OTLP traces (OTEL_EXPORTER_OTLP_TRACES_ENDPOINT)").version(C).argument("<command...>","MCP server command to wrap").action(t=>{const[s,...r]=t,i=function(e,t){const s=function(e,t){if(![e.split("/").pop()??e,...t].find(e=>U.has(e.replace(/@.*$/,""))))return null;const s=t.find(e=>/^https?:\/\//i.test(e));if(!s)return null;const r=t.join(" ");return{url:s,transport:/(^|\s)--sse(\s|$)/.test(r)||/--transport(\s+|=)sse/.test(r)||/\/sse(\?|$)/i.test(s)?"sse":"http"}}(e,t);if(s)return{name:n(s.url),version:void 0,transport:s.transport,url:s.url};const r=function(e){for(const t of e){if($.has(t))continue;const e=t.match(/^(@[^@]+\/[^@]+)@(.+)$/);if(e)return{packageName:e[1],version:e[2]};const s=t.match(/^([^@]+)@(.+)$/);if(s)return{packageName:s[1],version:s[2]};const r=t.match(/^(@[^@]+\/[^@]+)$/);if(r)return{packageName:r[1],version:"latest"};if(t.match(/^[a-z0-9-]+$/)&&!t.includes("/"))return{packageName:t,version:"latest"}}return null}(t.filter(e=>!e.startsWith("-")));let i,o;if(i=r?.packageName?r.packageName:e.split("/").pop()||e||"mcp-server-unknown",r&&(o=r.version,"latest"===r.version)){const t=G.has(e)?"pip":"npm",s=function(e,t){try{if("pip"===t){let t=null;for(const s of["pip3","pip"])try{t=q(`${s} index versions ${e}`,{encoding:"utf-8",timeout:5e3,stdio:["pipe","pipe","pipe"]});break}catch{continue}if(!t)return null;const s=t.match(/\(([^)]+)\)/);if(s)return s[1];const r=t.match(/Available versions:\s*([^\s,]+)/);return r?r[1]:null}{const t=q(`npm view ${e} version --json`,{encoding:"utf-8",timeout:5e3,stdio:["pipe","pipe","pipe"]}).trim().replace(/^["']|["']$/g,""),s=t.match(/^(\d+\.\d+\.\d+(?:-[\w.]+)?)/);return s?s[1]:t}}catch{return null}}(r.packageName,t);s&&(o=s)}return{name:i,version:o,transport:"stdio"}}(s,r);var o;o={serviceName:i.name,serviceVersion:i.version},L.initialize(o),e("Starting MCP server: "+t.join(" ")),e(`Service: ${i.name} = ${i.version||"unknown"}`),i.url&&e(`Bridged (${i.transport}) to ${i.url}`),function(e,t){L.setCLIServerInfo(e,t,{})}(s,r),new X(s,r,i)}),y.parse();
|
|
2
|
+
function e(...e){console.error("[mcp-logger]",...e)}function t(...e){N&&console.error("[mcp-logger]",...e)}function s(...e){b&&console.error("[mcp-logger]",...e)}function r(e){return!k.includes(e)}function n(e){try{const t=new URL(e),s=t.pathname.replace(/\/+$/,"");return`${t.host}${s}`}catch{return e}}function i(e,t){return{decision:"ALLOW",reason:null,durationMs:Date.now()-e,failOpenReason:t}}function o(e){const t=[];for(const[s,r]of Object.entries(e))null!=r&&("string"==typeof r?t.push({key:s,value:{stringValue:r}}):"boolean"==typeof r?t.push({key:s,value:{boolValue:r}}):Number.isInteger(r)?t.push({key:s,value:{intValue:r}}):t.push({key:s,value:{doubleValue:r}}));return t}function a(e){if(/^[0-9A-HJKMNP-TV-Z]{26}$/i.test(e)){let t=0n;for(const s of e.toUpperCase())t=t<<5n|BigInt("0123456789ABCDEFGHJKMNPQRSTVWXYZ".indexOf(s));return t&=(1n<<128n)-1n,t.toString(16).padStart(32,"0")}return A("sha256").update(e).digest("hex").slice(0,32)}function p(e,t){return A("sha256").update(`${e}|${t+""}`).digest("hex").slice(0,16)}function c(e){let t="";for(let s=0;e>s;s++)t+=Math.floor(256*Math.random()).toString(16).padStart(2,"0");return t}function u(e,t=2e5){try{const s=JSON.stringify(e);return s.length>t?s.slice(0,t)+"... (truncated)":s}catch{return null}}function d(e,t,s,r,n){const i=function(e,t){const s=e?.getClientInfo();if(s?.name||s?.version)return{name:s.name,version:s.version};const r=e?.getHeaders()||t;if(r){const e=r["user-agent"]||r["User-Agent"];if(e){const t=e.match(/^([^\/]+)\/([^\s]+)/);if(t)return{name:t[1],version:t[2]}}}return{name:void 0,version:void 0}}(r,n),o=r?function(e){let t=j.get(e);return t||(t=new B,j.set(e,t)),t}(r):J,a=`${i.name||"unknown"}-${i.version||"0"}`,p=o.getOrCreateSessionId(r?"connection":a),c=o.getOrCreateConversationId(p,e),u=function(e,t){return e?.userId||e?.user_id?e.userId||e.user_id:t?t["x-user-id"]||t["X-User-Id"]:void 0}(t,n),d=function(e){if(e?.permission||e?.permissionLevel)return e.permission||e.permissionLevel}(t),h=function(e,t){return void 0!==e?.cost?e.cost:void 0!==t?.cost?t.cost:void 0}(t,s),l={};return p&&(l.sessionId=p),c&&(l.conversationId=c),u&&(l.userId=u),i.name&&(l.clientName=i.name),i.version&&(l.clientVersion=i.version),d&&(l.permissionLevel=d),void 0!==h&&(l.cost=h),l}function h(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}function l(e){if("string"!=typeof e)return;const t=e.trim();return!t||"unknown"===t.toLowerCase()||t.startsWith("{")||t.startsWith("[")?void 0:t}function m(e){if(h(e))return l(e["sh.pinta/model-provider"])}function f(e,t,s){if(null==e.id)return;if("response"===t&&"sampling/createMessage"===e.method){if(s?.id!==e.id||void 0!==s.error||!h(s.result))return;const t=l(s.result.model);if(void 0===t)return;return{model:t,source:"sampling_response",provider:m(s.result._meta)}}if("request"!==t||"tools/call"!==e.method||!h(e.params))return;const r=e.params._meta;if(!h(r))return;const n=l(r["sh.pinta/model"]);return void 0!==n?{model:n,source:"pinta_request_meta",provider:m(r)}:void 0}function v(e,t={}){if(0===e.length)return e;const s=function(e,t){const s=[];for(const r of V){if(r.requireContext&&r.requireContext!==t.context)continue;const n=RegExp(r.regex.source,r.regex.flags);let i;for(;null!==(i=n.exec(e));){const e=i[r.captureGroup??0];if(void 0===e){i.index===n.lastIndex&&n.lastIndex++;continue}const t=i.index,o=i.index+i[0].length,a=t+i[0].indexOf(e),p=a+e.length;s.push({start:t,end:o,replaceStart:a,replaceEnd:p,type:r.type}),i.index===n.lastIndex&&n.lastIndex++}}return s}(e,t);return 0===s.length?e:function(e,t){const s=[...t].sort((e,t)=>t.replaceStart-e.replaceStart);let r=e;for(const e of s)r=r.slice(0,e.replaceStart)+`[PINTA_REDACTED:${e.type}]`+r.slice(e.replaceEnd);return r}(e,function(e){const t=[...e].sort((e,t)=>e.start!==t.start?e.start-t.start:t.end-t.start-(e.end-e.start)),s=[];let r=-1;for(const e of t)e.start<r||(s.push(e),r=e.end);return s}(s))}function g(e){F.build(e)}import{program as y}from"commander";import{NodeSDK as I}from"@opentelemetry/sdk-node";import{OTLPTraceExporter as x}from"@opentelemetry/exporter-trace-otlp-http";import{Resource as S}from"@opentelemetry/resources";import{trace as w,SpanStatusCode as D,context as E,TraceFlags as _}from"@opentelemetry/api";import{execSync as q,spawn as M}from"child_process";import{Transform as T}from"stream";import{createHash as A}from"node:crypto";import{ulid as R}from"ulid";const C="0.3.2",N="true"===process.env.PINTA_DEBUG||"1"===process.env.PINTA_DEBUG,b="true"===process.env.PINTA_RAW_DEBUG||"1"===process.env.PINTA_RAW_DEBUG,O=C,k=["notifications/initialized"],P=process.env.PINTA_GUARD_ENDPOINT;!function(){if(process.env.PINTA_RELAY_TOKEN)return;const e=(process.env.OTEL_EXPORTER_OTLP_HEADERS??"").match(/x-pinta-relay-token=([^,\s]+)/i);e&&(process.env.PINTA_RELAY_TOKEN=e[1])}();const L=new class{sdk=null;tracer=null;initialized=!1;cliServerInfo=null;resourceAttrs={"telemetry.sdk.name":"mcp-logger","telemetry.sdk.version":O};initialize(e){if(this.initialized)return t("Telemetry already initialized, returning existing tracer"),this.tracer;const s=e.serviceName,r=e.serviceVersion||"unknown";t("Initializing telemetry..."),t("Service: "+s),t("Version: "+r);const n={...this.resourceAttrs,"service.name":s,"service.version":r};this.resourceAttrs=n,this.sdk=new I({resource:new S(n),traceExporter:new x}),this.sdk.start(),this.tracer=w.getTracer("mcp-logger",O),this.initialized=!0,t("Telemetry initialized successfully");const i=async()=>{t("Shutting down telemetry..."),await this.shutdown()};return process.once("SIGTERM",i),process.once("SIGINT",i),this.tracer}getTracer(){return this.tracer}isInitialized(){return this.initialized}getResourceAttributes(){return{...this.resourceAttrs}}setCLIServerInfo(e,t,s){this.cliServerInfo={command:e,args:t,env:s}}getCLIServerInfo(){return this.cliServerInfo}async shutdown(){if(this.sdk)try{await this.sdk.shutdown(),this.sdk=null,this.tracer=null,this.initialized=!1,this.cliServerInfo=null}catch(e){console.error("[mcp-logger] Error shutting down telemetry:",e)}}},U=new Set(["mcp-remote","supergateway","mcp-proxy"]),$=new Set(["npx","node","uvx","python","python3","pip","pipx","deno","bun"]),G=new Set(["uvx","python","python3","pip","pipx"]),z="x-pinta-guard-budget-ms",Z="mcp-logger/"+C;class B{sessionMap=new Map;sessionCounter=0;conversationMap=new Map;conversationCounter=0;getOrCreateSessionId(e){if(!this.sessionMap.has(e)){const t=R();this.sessionMap.set(e,t),this.sessionCounter++}return this.sessionMap.get(e)}getOrCreateConversationId(e,t){const s=`${e}-${t}`;return this.conversationMap.has(s)||this.conversationMap.set(s,"conv-"+ ++this.conversationCounter),this.conversationMap.get(s)}clear(){this.sessionMap.clear(),this.conversationMap.clear(),this.sessionCounter=0,this.conversationCounter=0}}const J=new B;let j=new WeakMap;const V=[{type:"aws_access_key",regex:/AKIA[0-9A-Z]{16}/g},{type:"aws_secret_key",regex:/(?:aws[_-]?secret(?:[_-]?(?:access)?[_-]?key)?)\s*[:=]\s*["']?([A-Za-z0-9/+=]{40})(?![A-Za-z0-9/+=])/gi,captureGroup:1},{type:"gcp_service_account",regex:/\{[\s\S]{0,200}?"type"\s*:\s*"service_account"[\s\S]*?\}/g},{type:"github_token",regex:/gh[pousr]_[A-Za-z0-9]{36,}/g},{type:"gitlab_token",regex:/glpat-[A-Za-z0-9_-]{20}/g},{type:"slack_token",regex:/xox[abrsp]-[0-9A-Za-z-]{10,}/g},{type:"openai_key",regex:/sk-(?:proj-)?[A-Za-z0-9_-]{40,}/g},{type:"anthropic_key",regex:/sk-ant-[A-Za-z0-9_-]{50,}/g},{type:"stripe_key",regex:/(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{20,}/g},{type:"jwt",regex:/eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g},{type:"private_key_block",regex:/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g},{type:"bearer_token",regex:/bearer\s+([A-Za-z0-9._~+/=-]{12,})/gi,captureGroup:1},{type:"basic_auth",regex:/basic\s+([A-Za-z0-9+/=]{12,})/gi,captureGroup:1},{type:"db_url_password",regex:/\b(?:postgres|postgresql|mysql|mariadb|mongodb(?:\+srv)?|redis):\/\/[^:\s/]+:([^@\s]+)@/gi,captureGroup:1},{type:"cli_password_flag",regex:/(?:--password|--pass|--pwd)[=\s]([^\s'"]+)/g,captureGroup:1},{type:"cli_password_short",regex:/\s-p([^\s'"]+)/g,captureGroup:1,requireContext:"bash"},{type:"env_var_secret",regex:/^(?:export\s+)?([A-Z][A-Z0-9_]*(?:KEY|SECRET|TOKEN|PASSWORD|PASSWD|PWD|API_KEY)[A-Z0-9_]*)\s*=\s*["']?([^\s"'\n]+)/gm,captureGroup:2}];class W{spanData;input;contextMetadata;buildPayload(e){const t=this.buildAttributes(e),s=this.contextMetadata.sessionId;return function(e){const t=e.now??Date.now(),s=""+1000000n*BigInt(t),r=[];for(const[t,s]of Object.entries(e.resource))void 0!==s&&r.push({key:t,value:{stringValue:s}});const n={traceId:e.traceId??c(16),spanId:e.spanId??c(8),...e.parentSpanId?{parentSpanId:e.parentSpanId}:{},name:e.name,kind:1,startTimeUnixNano:s,endTimeUnixNano:s,attributes:o(e.attributes)};return{resourceSpans:[{resource:{attributes:r},scopeSpans:[{scope:e.scope,spans:[n]}]}]}}({name:"mcp."+e.method,attributes:t,resource:L.getResourceAttributes(),scope:{name:"mcp-logger",version:C},...s?{traceId:a(s)}:{},...s&&void 0!==e.requestId?{spanId:p(s,this.exchangeId())}:{},now:e.requestTimestamp})}buildAttributes(e){if(this.input=e,this.spanData=this.buildBase(),this.resolveContextMetadata(),this.applyRequestFields(),this.applyResponseFields(),this.applyDuration(),this.applyCLIServerInfo(),this.applyMCPServerInfo(),this.applyContextMetadata(),this.applyGuard(),this.applyModelEvidence(),this.input.customEvents&&this.input.customEvents.length>0&&(this.spanData["mcp.events"]=JSON.stringify(this.input.customEvents),this.spanData["mcp.events.count"]=this.input.customEvents.length),e.error){this.spanData["mcp.error"]=!0;const t="string"==typeof e.error?e.error:e.error instanceof Error?e.error.message:u(e.error)||"Unknown error";this.spanData["mcp.error.message"]=t}return{...this.spanData}}build(e){const t=L.getTracer();if(!t)return;this.input=e,this.resolveContextMetadata();const r=this.parentContext();t.startActiveSpan("mcp."+e.method,{},r,e=>{try{this.spanData=this.buildBase(),this.applyRequestFields(),this.applyResponseFields(),this.applyDuration(),this.applyCLIServerInfo(),this.applyMCPServerInfo(),this.applyContextMetadata(),this.applyGuard(),this.applyModelEvidence(),this.applyCustomEvents(e),this.applyError(e),function(e,t){for(const[s,r]of Object.entries(t))null!=r&&e.setAttribute(s,r)}(e,this.spanData),s("[OTLP] Span data:",JSON.stringify(this.spanData,null,2))}catch{e.setStatus({code:D.ERROR,message:"Error creating span"})}finally{e.end()}})}buildBase(){const e={"ingest.type":"mcp","mcp.type":this.input.type,"mcp.transport":this.input.transport,"mcp.method":this.input.method};this.input.serverUrl&&(e["mcp.server.url"]=this.input.serverUrl);const t=function(e=process.env){const t=e.PINTA_MCP_CONFIG_KEY;if("string"!=typeof t)return;const s=t.trim();return s.length>0?s:void 0}();return t&&(e["mcp.server.config_key"]=t),e}parentContext(){const e=this.contextMetadata.sessionId;if(!e)return E.active();const t=this.exchangeId();return w.setSpanContext(E.active(),{traceId:a(e),spanId:p(e,void 0===t?"session":t),traceFlags:_.SAMPLED,isRemote:!0})}exchangeId(){const e=this.input.requestId??this.input.responseId;return void 0!==e&&this.input.requestDirection?`${this.input.requestDirection}:${typeof e}:${e}`:e}resolveContextMetadata(){this.contextMetadata=this.input.contextMetadata||d(this.input.method,this.input.params,this.input.result,void 0,this.input.headers)}applyRequestFields(){if("response"!==this.input.type){if("paired"===this.input.type||"request"===this.input.type||"notification"===this.input.type){if(void 0!==this.input.requestId&&(this.spanData["mcp.request.id"]=this.input.requestId+""),this.spanData["mcp.request.method"]=this.input.method,void 0!==this.input.requestTimestamp&&(this.spanData["mcp.request.timestamp"]=this.input.requestTimestamp),void 0!==this.input.params){const e=u(this.input.params)||"{}";this.spanData["mcp.request.params"]=v(e,{context:"bash"})}if(this.input.headers){const e=u(this.input.headers);e&&(this.spanData["mcp.headers"]=e)}}}else void 0!==this.input.requestId&&(this.spanData["mcp.request.id"]=this.input.requestId+"")}applyResponseFields(){if(("paired"===this.input.type||"response"===this.input.type)&&(void 0!==this.input.responseId&&(this.spanData["mcp.response.id"]=this.input.responseId+""),this.spanData["mcp.response.method"]=this.input.method,void 0!==this.input.responseTimestamp&&(this.spanData["mcp.response.timestamp"]=this.input.responseTimestamp),void 0!==this.input.result)){const e=u(this.input.result);e&&(this.spanData["mcp.response.result"]=v(e,{context:"bash"}))}}applyDuration(){void 0!==this.input.durationMs&&(this.spanData["mcp.duration_ms"]=this.input.durationMs)}applyCLIServerInfo(){const e=L.getCLIServerInfo();e&&(this.spanData["mcp.cli"]=JSON.stringify({command:e.command,args:e.args,env:e.env}))}applyMCPServerInfo(){this.input.serverInfo&&(this.input.serverInfo.name&&(this.spanData["mcp.sdk.name"]=this.input.serverInfo.name),this.input.serverInfo.version&&(this.spanData["mcp.sdk.version"]=this.input.serverInfo.version))}applyContextMetadata(){this.contextMetadata.sessionId&&(this.spanData["mcp.session.id"]=this.contextMetadata.sessionId),this.contextMetadata.conversationId&&(this.spanData["mcp.conversation.id"]=this.contextMetadata.conversationId),this.contextMetadata.userId&&(this.spanData["mcp.user.id"]=this.contextMetadata.userId),this.contextMetadata.clientName&&(this.spanData["mcp.client.name"]=this.contextMetadata.clientName),this.contextMetadata.clientVersion&&(this.spanData["mcp.client.version"]=this.contextMetadata.clientVersion),this.contextMetadata.permissionLevel&&(this.spanData["mcp.permission.level"]=this.contextMetadata.permissionLevel),void 0!==this.contextMetadata.cost&&(this.spanData["mcp.cost"]=this.contextMetadata.cost)}applyGuard(){const e=this.input.guard;e&&(this.spanData["pinta.guard.decision"]=e.decision.toLowerCase(),this.spanData["pinta.guard.duration_ms"]=Math.round(e.durationMs),e.reason&&(this.spanData["pinta.guard.matched_rule"]=e.reason),e.failOpenReason&&(this.spanData["pinta.guard.fail_open_reason"]=e.failOpenReason))}applyModelEvidence(){const e=this.input.modelEvidence,t=l(e?.model);if(!e||void 0===t)return;this.spanData["mcp.model"]=t,this.spanData["mcp.model_source"]=e.source;const s=l(e.provider);void 0!==s&&(this.spanData["mcp.model_provider"]=s)}applyCustomEvents(e){this.input.customEvents&&0!==this.input.customEvents.length&&(this.spanData["mcp.events"]=JSON.stringify(this.input.customEvents),this.spanData["mcp.events.count"]=this.input.customEvents.length,this.input.customEvents.forEach(t=>{e.addEvent("custom."+(t.level||"info"),{message:t.message,metadata:JSON.stringify(t.metadata||{}),timestamp:t.timestamp||Date.now()})}))}applyError(e){if(this.input.error){let t;this.spanData["mcp.error"]=!0,this.input.error instanceof Error?(t=this.input.error.message,e.recordException(this.input.error)):t="string"==typeof this.input.error?this.input.error:u(this.input.error)||"Unknown error",this.spanData["mcp.error.message"]=t,e.setStatus({code:D.ERROR,message:t})}else e.setStatus({code:D.OK})}}const F=new W,K=new Set(["tools/call","resources/read","prompts/get"]);class Y extends T{buffer="";direction;logger;sessionContextStore;constructor(e,t,s){super(),this.direction=e,this.logger=t,this.sessionContextStore=s}_transform(e,s,r){try{if("request"===this.direction){this.buffer+=e.toString();const t=this.buffer.split("\n");return this.buffer=t.pop()||"",void(async()=>{for(const e of t)await this.processRequestLine(e)})().then(()=>r()).catch(e=>r(e))}this.buffer+=e.toString(),this.buffer.length>1e6&&(t("Buffer size exceeded, clearing..."),this.buffer=""),this.processBuffer(!0),r()}catch(e){r(e)}}processBuffer(e){const r=this.buffer.split("\n");this.buffer=r.pop()||"";for(const n of r){const r=n.trim();if(r)try{const n=JSON.parse(r);if(e){if("2.0"!==n.jsonrpc){t("Non-JSON-RPC JSON ignored:",r.slice(0,100));continue}this.push(r+"\n")}s(`[${this.direction}] Raw message:`,r),this.handleMessage(n)}catch{t(e?"Non-JSON output filtered:":"Failed to parse:",r.slice(0,100))}}}handleMessage(e){e.method?this.logger.onRequest(e,void 0,this.direction):null!=e.id&&this.logger.onResponse(e,this.direction)}async processRequestLine(e,r="\n"){const n=e.trim();if(!n)return;let i;try{i=JSON.parse(n)}catch{return t("Failed to parse:",n.slice(0,100)),void this.push(e+r)}if(i?.method&&K.has(i.method)){const e=i,t=await this.evaluateRequestGuard(e);if("DENY"===t?.decision){const r={jsonrpc:"2.0",id:e.id??null,error:{code:-32e3,message:"Pinta guard denied: "+(t.reason??"guard_deny"),data:{reason:t.reason}}};return process.stdout.write(JSON.stringify(r)+"\n"),s(`[${this.direction}] DENY:`,n),void this.logger.onRequestDenied(e,t)}this.logger.onRequest(e,t??void 0)}else i&&this.handleMessage(i);this.push(e+r),s(`[${this.direction}] Raw message:`,n)}async evaluateRequestGuard(e){const t=process.env.PINTA_GUARD_ENDPOINT??P;return t&&"1"!==process.env.PINTA_GUARD_DISABLED?async function(e,t){if(!t)return null;if("1"===process.env.PINTA_GUARD_DISABLED)return null;const s=Date.now(),r=new AbortController,n=setTimeout(()=>r.abort(),100);try{const n=await fetch(t,{method:"POST",headers:{"content-type":"application/json","user-agent":Z,"x-pinta-relay-token":process.env.PINTA_RELAY_TOKEN??"",[z]:"100"},body:JSON.stringify(e),signal:r.signal});if(410===n.status)return i(s,"refused");if(200!==n.status)return i(s,"error");const o=await n.json();return!o||"ALLOW"!==o.decision&&"DENY"!==o.decision&&"REVIEW"!==o.decision?i(s,"error"):{decision:o.decision,reason:o.reason??null,durationMs:o.durationMs??Date.now()-s}}catch(e){const t=e.name;return i(s,"AbortError"===t||"TimeoutError"===t?"timeout":"error")}finally{clearTimeout(n)}}((new W).buildPayload({type:"request",method:e.method,transport:this.logger.transport,serverUrl:this.logger.serverUrl,requestId:e.id??void 0,requestDirection:"request",requestTimestamp:Date.now(),params:e.params,serverInfo:this.sessionContextStore.getServerInfo(),contextMetadata:d(e.method,e.params,void 0,this.sessionContextStore),modelEvidence:f(e,"request")}),t):null}_flush(e){const s=()=>{N&&t("Stream closed. Pending requests: "+this.logger.getStats().pending),e()};try{if("request"===this.direction){const t=this.buffer;return this.buffer="",void this.processRequestLine(t,"").then(s,t=>e(t))}if(this.buffer.trim())try{const e=JSON.parse(this.buffer);"response"===this.direction&&"2.0"===e.jsonrpc&&this.push(this.buffer.trim()+"\n"),this.handleMessage(e)}catch{t("Failed to parse final buffer")}s()}catch(t){e(t)}}}class H{pendingRequests={request:new Map,response:new Map};TIMEOUT=3e4;sessionContextStore;transport;serverUrl;constructor(e,t={}){this.sessionContextStore=e,this.transport=t.transport??"stdio",this.serverUrl=t.serverUrl}onRequest(e,t,s="request"){if(!r(e.method))return;if(null==e.id)return void this.onNotification(e);"request"===s&&"initialize"===e.method&&e.params?.clientInfo&&this.sessionContextStore.setClientInfo({name:e.params.clientInfo.name,version:e.params.clientInfo.version});const n=this.pendingRequests[s],i=n.get(e.id);i&&(clearTimeout(i.timeout),clearTimeout(i.sweep));const o={request:e,timestamp:Date.now(),guard:t,direction:s,timeout:setTimeout(()=>this.handleTimeout(e.id,s),this.TIMEOUT)};o.timeout.unref?.(),n.set(e.id,o)}onRequestDenied(e,t){if(!r(e.method))return;const s=d(e.method,e.params,void 0,this.sessionContextStore,void 0);g({type:"request",method:e.method,transport:this.transport,serverUrl:this.serverUrl,requestId:e.id??void 0,requestDirection:"request",requestTimestamp:Date.now(),params:e.params,contextMetadata:s,guard:t,modelEvidence:f(e,"request")})}onResponse(e,t="response"){if(null==e.id)return;const s="response"===t?"request":"response",r=this.pendingRequests[s],n=r.get(e.id),i=Date.now(),o=n?.request.method||e.method||"unknown";let a=this.sessionContextStore.getServerInfo();"request"===s&&"initialize"===n?.request.method&&e.result?.serverInfo&&(a={name:e.result.serverInfo.name,version:e.result.serverInfo.version},this.sessionContextStore.setServerInfo(a));const p=d(o,n?.request.params,e.result,this.sessionContextStore,void 0);n?(clearTimeout(n.timeout),clearTimeout(n.sweep),r.delete(e.id),g({type:n.timedOut?"response":"paired",method:o,transport:this.transport,serverUrl:this.serverUrl,requestId:n.request.id,requestDirection:s,requestTimestamp:n.timedOut?void 0:n.timestamp,params:n.timedOut?void 0:n.request.params,responseId:e.id,responseTimestamp:i,result:e.result,error:e.error,durationMs:i-n.timestamp,contextMetadata:p,serverInfo:a,guard:n.guard,modelEvidence:f(n.request,s,e)})):g({type:"response",method:o,transport:this.transport,serverUrl:this.serverUrl,responseId:e.id,requestDirection:s,responseTimestamp:i,result:e.result,error:e.error,contextMetadata:p,serverInfo:a})}onNotification(e){if(!r(e.method))return;const t=d(e.method,e.params,void 0,this.sessionContextStore,void 0);g({type:"notification",method:e.method,transport:this.transport,serverUrl:this.serverUrl,params:e.params,contextMetadata:t})}handleTimeout(e,t){const s=this.pendingRequests[t],r=s.get(e);r&&!r.timedOut&&(this.emitRequestSpan(r),r.timedOut=!0,r.sweep=setTimeout(()=>{s.get(e)===r&&s.delete(e)},6e5),r.sweep.unref?.())}emitRequestSpan(e){const t=d(e.request.method,e.request.params,void 0,this.sessionContextStore,void 0);g({type:"request",method:e.request.method,transport:this.transport,serverUrl:this.serverUrl,requestId:e.request.id,requestDirection:e.direction,requestTimestamp:e.timestamp,params:e.request.params,contextMetadata:t,guard:e.guard,modelEvidence:f(e.request,e.direction)})}flushPending(){for(const e of Object.values(this.pendingRequests))for(const t of e.values())t.timedOut||this.emitRequestSpan(t);this.clear()}getStats(){return{pending:this.pendingRequests.request.size+this.pendingRequests.response.size}}clear(){for(const e of Object.values(this.pendingRequests)){for(const t of e.values())clearTimeout(t.timeout),clearTimeout(t.sweep);e.clear()}}}class X{context={};getClientInfo(){return this.context.clientInfo}setClientInfo(e){this.context.clientInfo=e}getServerInfo(){return this.context.serverInfo}setServerInfo(e){this.context.serverInfo=e}getHeaders(){return this.context.headers}setHeaders(e){this.context.headers=e}snapshot(){return{clientInfo:this.context.clientInfo?{...this.context.clientInfo}:void 0,serverInfo:this.context.serverInfo?{...this.context.serverInfo}:void 0,headers:this.context.headers?{...this.context.headers}:void 0}}}class Q{process;logger;isShuttingDown=!1;constructor(e,t,s){const r=new X;this.logger=new H(r,{transport:s.transport,serverUrl:s.url}),this.process=M(e,t,{stdio:["pipe","pipe","inherit"],env:{...process.env,MCP_LOGGER_ENABLED:"true"}});const n=new Y("request",this.logger,r),i=new Y("response",this.logger,r);process.stdin.pipe(n).pipe(this.process.stdin),this.process.stdout.pipe(i).pipe(process.stdout),this.process.on("error",e=>this.handleError(e)),this.process.on("exit",(e,t)=>this.handleExit(e,t)),process.on("SIGTERM",()=>this.shutdown("SIGTERM")),process.on("SIGINT",()=>this.shutdown("SIGINT"))}async handleError(t){e("Failed to start MCP server:",t),await this.cleanup(),process.exit(1)}async handleExit(e,s){t(`MCP server exited with code ${e}, signal ${s}`),await this.cleanup(),process.exit(e||0)}async shutdown(e){this.isShuttingDown||(this.isShuttingDown=!0,t(`Received ${e}, shutting down...`),this.process.kill(e),await Promise.race([new Promise(e=>this.process.once("exit",e)),new Promise(e=>setTimeout(e,5e3))]),await this.cleanup(),process.exit(0))}async cleanup(){this.logger.flushPending(),await async function(){await L.shutdown()}()}}y.name("mcp-logger").description("Wrap an MCP server and forward OTLP traces (OTEL_EXPORTER_OTLP_TRACES_ENDPOINT)").version(C).argument("<command...>","MCP server command to wrap").action(t=>{const[s,...r]=t,i=function(e,t){const s=function(e,t){if(![e.split("/").pop()??e,...t].find(e=>U.has(e.replace(/@.*$/,""))))return null;const s=t.find(e=>/^https?:\/\//i.test(e));if(!s)return null;const r=t.join(" ");return{url:s,transport:/(^|\s)--sse(\s|$)/.test(r)||/--transport(\s+|=)sse/.test(r)||/\/sse(\?|$)/i.test(s)?"sse":"http"}}(e,t);if(s)return{name:n(s.url),version:void 0,transport:s.transport,url:s.url};const r=function(e){for(const t of e){if($.has(t))continue;const e=t.match(/^(@[^@]+\/[^@]+)@(.+)$/);if(e)return{packageName:e[1],version:e[2]};const s=t.match(/^([^@]+)@(.+)$/);if(s)return{packageName:s[1],version:s[2]};const r=t.match(/^(@[^@]+\/[^@]+)$/);if(r)return{packageName:r[1],version:"latest"};if(t.match(/^[a-z0-9-]+$/)&&!t.includes("/"))return{packageName:t,version:"latest"}}return null}(t.filter(e=>!e.startsWith("-")));let i,o;if(i=r?.packageName?r.packageName:e.split("/").pop()||e||"mcp-server-unknown",r&&(o=r.version,"latest"===r.version)){const t=G.has(e)?"pip":"npm",s=function(e,t){try{if("pip"===t){let t=null;for(const s of["pip3","pip"])try{t=q(`${s} index versions ${e}`,{encoding:"utf-8",timeout:5e3,stdio:["pipe","pipe","pipe"]});break}catch{continue}if(!t)return null;const s=t.match(/\(([^)]+)\)/);if(s)return s[1];const r=t.match(/Available versions:\s*([^\s,]+)/);return r?r[1]:null}{const t=q(`npm view ${e} version --json`,{encoding:"utf-8",timeout:5e3,stdio:["pipe","pipe","pipe"]}).trim().replace(/^["']|["']$/g,""),s=t.match(/^(\d+\.\d+\.\d+(?:-[\w.]+)?)/);return s?s[1]:t}}catch{return null}}(r.packageName,t);s&&(o=s)}return{name:i,version:o,transport:"stdio"}}(s,r);var o;o={serviceName:i.name,serviceVersion:i.version},L.initialize(o),e("Starting MCP server: "+t.join(" ")),e(`Service: ${i.name} = ${i.version||"unknown"}`),i.url&&e(`Bridged (${i.transport}) to ${i.url}`),function(e,t){L.setCLIServerInfo(e,t,{})}(s,r),new Q(s,r,i)}),y.parse();
|
package/package.json
CHANGED