@pinta-ai/mcp-logger 0.3.2 → 0.3.3

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/README.md CHANGED
@@ -57,6 +57,38 @@ The guard is asked about the span itself: the request half of the exchange (`mcp
57
57
 
58
58
  Only **DENY** blocks: the client receives a JSON-RPC error with its original request ID, code `-32000`, message `Pinta guard denied: <reason>`, and `error.data.reason`. The server never receives the denied request. **ALLOW** and **REVIEW** continue normally. An unavailable guard retains the existing fail-open behavior; an unset endpoint or `PINTA_GUARD_DISABLED=1` disables guard evaluation entirely.
59
59
 
60
+ ### Correlated response-output guard
61
+
62
+ The responses to those same three methods are evaluated **before response bytes
63
+ reach the client**, including MCP `isError` results and JSON-RPC errors. The
64
+ guard receives a native `mcp.type=response` span with the correlated request
65
+ method, params and ID plus the original result/error. It must evaluate output
66
+ policies, not re-run operation policies as if the completed request were pending.
67
+ The existing Manager 0.1.11 floor establishes envelope compatibility, not this
68
+ returned-content capability; output/error projection and output-only policy
69
+ evaluation must also be supported.
70
+
71
+ On DENY, the wrapper returns a JSON-RPC error (`-32000`) with the exact original
72
+ ID and `error.data.phase=tool_output`, replacing the entire original response.
73
+ Its message and `error.data.reason=tool_output_denied` are fixed safe text,
74
+ not guard-supplied content reflected back to the model.
75
+ It does not claim that the server operation was prevented or undone. The
76
+ original masked exchange remains the audit evidence, with
77
+ `pinta.guard.target=tool_output` and the pre-request verdict retained separately
78
+ as `pinta.guard.request_decision`, `request_duration_ms`, and, when present,
79
+ `request_matched_rule` / `request_fail_open_reason` under the same prefix.
80
+ No collector ACK is awaited by this decision.
81
+ Successful server execution is not relabelled as a failed execution merely
82
+ because result delivery was denied. Native `tools/call` `isError: true` and
83
+ JSON-RPC errors retain error execution status and their original masked evidence,
84
+ independently of the output-delivery verdict.
85
+
86
+ Correlation is connection-local, exact-ID-type and direction-aware, including
87
+ the existing late-response window. Unknown/expired responses, discovery and
88
+ notifications remain ungated; server-initiated sampling is not an enclosing
89
+ tool's output. EOF and fragmented responses use the same gate. This is MCP
90
+ coverage only: it does not block Cursor's native Shell/Write operations.
91
+
60
92
  ## What's emitted
61
93
 
62
94
  OTLP HTTP+JSON traces with:
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 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();
2
+ function e(...e){console.error("[mcp-logger]",...e)}function t(...e){O&&console.error("[mcp-logger]",...e)}function s(...e){k&&console.error("[mcp-logger]",...e)}function r(e){return!L.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}}async function o(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":J,"x-pinta-relay-token":process.env.PINTA_RELAY_TOKEN??"",[B]:"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)}}function a(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 p(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 C("sha256").update(e).digest("hex").slice(0,32)}function u(e,t){return C("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 d(e,t=2e5){try{const s=JSON.stringify(e);return s.length>t?s.slice(0,t)+"... (truncated)":s}catch{return null}}function h(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=F.get(e);return t||(t=new j,F.set(e,t)),t}(r):V,a=`${i.name||"unknown"}-${i.version||"0"}`,p=o.getOrCreateSessionId(r?"connection":a),u=o.getOrCreateConversationId(p,e),c=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),u&&(l.conversationId=u),c&&(l.userId=c),i.name&&(l.clientName=i.name),i.version&&(l.clientVersion=i.version),d&&(l.permissionLevel=d),void 0!==h&&(l.cost=h),l}function l(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}function m(e){if("string"!=typeof e)return;const t=e.trim();return!t||"unknown"===t.toLowerCase()||t.startsWith("{")||t.startsWith("[")?void 0:t}function f(e){if(l(e))return m(e["sh.pinta/model-provider"])}function v(e,t,s){if(null==e.id)return;if("response"===t&&"sampling/createMessage"===e.method){if(s?.id!==e.id||void 0!==s.error||!l(s.result))return;const t=m(s.result.model);if(void 0===t)return;return{model:t,source:"sampling_response",provider:f(s.result._meta)}}if("request"!==t||"tools/call"!==e.method||!l(e.params))return;const r=e.params._meta;if(!l(r))return;const n=m(r["sh.pinta/model"]);return void 0!==n?{model:n,source:"pinta_request_meta",provider:f(r)}:void 0}function g(e,t={}){if(0===e.length)return e;const s=function(e,t){const s=[];for(const r of W){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 y(e){Y.build(e)}import{program as I}from"commander";import{NodeSDK as x}from"@opentelemetry/sdk-node";import{OTLPTraceExporter as S}from"@opentelemetry/exporter-trace-otlp-http";import{Resource as D}from"@opentelemetry/resources";import{trace as _,SpanStatusCode as q,context as w,TraceFlags as E}from"@opentelemetry/api";import{execSync as M,spawn as T}from"child_process";import{finished as R}from"stream/promises";import{Transform as A}from"stream";import{createHash as C}from"node:crypto";import{ulid as N}from"ulid";const b="0.3.3",O="true"===process.env.PINTA_DEBUG||"1"===process.env.PINTA_DEBUG,k="true"===process.env.PINTA_RAW_DEBUG||"1"===process.env.PINTA_RAW_DEBUG,P=b,L=["notifications/initialized"],G=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 U=new class{sdk=null;tracer=null;initialized=!1;cliServerInfo=null;resourceAttrs={"telemetry.sdk.name":"mcp-logger","telemetry.sdk.version":P};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 x({resource:new D(n),traceExporter:new S}),this.sdk.start(),this.tracer=_.getTracer("mcp-logger",P),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)}}},$=new Set(["mcp-remote","supergateway","mcp-proxy"]),z=new Set(["npx","node","uvx","python","python3","pip","pipx","deno","bun"]),Z=new Set(["uvx","python","python3","pip","pipx"]),B="x-pinta-guard-budget-ms",J="mcp-logger/"+b;class j{sessionMap=new Map;sessionCounter=0;conversationMap=new Map;conversationCounter=0;getOrCreateSessionId(e){if(!this.sessionMap.has(e)){const t=N();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 V=new j;let F=new WeakMap;const W=[{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 K{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:a(e.attributes)};return{resourceSpans:[{resource:{attributes:r},scopeSpans:[{scope:e.scope,spans:[n]}]}]}}({name:"mcp."+e.method,attributes:t,resource:U.getResourceAttributes(),scope:{name:"mcp-logger",version:b},...s?{traceId:p(s)}:{},...s&&void 0!==e.requestId?{spanId:u(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.nativeToolFailed()){this.spanData["mcp.error"]=!0;const t=e.error?"string"==typeof e.error?e.error:e.error instanceof Error?e.error.message:d(e.error)||"Unknown error":"MCP tool returned isError";this.spanData["mcp.error.message"]=g(t,{context:"bash"})}return{...this.spanData}}build(e){const t=U.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:q.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 w.active();const t=this.exchangeId();return _.setSpanContext(w.active(),{traceId:p(e),spanId:u(e,void 0===t?"session":t),traceFlags:E.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||h(this.input.method,this.input.params,this.input.result,void 0,this.input.headers)}applyRequestFields(){if("response"!==this.input.type||void 0!==this.input.requestId){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=d(this.input.params)||"{}";this.spanData["mcp.request.params"]=g(e,{context:"bash"})}if(this.input.headers){const e=d(this.input.headers);e&&(this.spanData["mcp.headers"]=e)}}}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=d(this.input.result);e&&(this.spanData["mcp.response.result"]=g(e,{context:"bash"}))}}applyDuration(){void 0!==this.input.durationMs&&(this.spanData["mcp.duration_ms"]=this.input.durationMs)}applyCLIServerInfo(){const e=U.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.outputGuard??this.input.guard;if(e){if(this.input.outputGuard){this.spanData["pinta.guard.target"]="tool_output";const e=this.input.guard;e&&(this.spanData["pinta.guard.request_decision"]=e.decision.toLowerCase(),this.spanData["pinta.guard.request_duration_ms"]=Math.round(e.durationMs),e.reason&&(this.spanData["pinta.guard.request_matched_rule"]=e.reason),e.failOpenReason&&(this.spanData["pinta.guard.request_fail_open_reason"]=e.failOpenReason))}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=m(e?.model);if(!e||void 0===t)return;this.spanData["mcp.model"]=t,this.spanData["mcp.model_source"]=e.source;const s=m(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()})}))}nativeToolFailed(){const e=this.input.result;return"tools/call"===this.input.method&&null!==e&&"object"==typeof e&&"isError"in e&&!0===e.isError}applyError(e){if(this.input.error||this.nativeToolFailed()){let t;this.spanData["mcp.error"]=!0,this.input.error?this.input.error instanceof Error?(t=this.input.error.message,e.recordException({name:this.input.error.name,message:g(t,{context:"bash"}),stack:this.input.error.stack?g(this.input.error.stack,{context:"bash"}):void 0})):t="string"==typeof this.input.error?this.input.error:d(this.input.error)||"Unknown error":t="MCP tool returned isError";const s=g(t,{context:"bash"});this.spanData["mcp.error.message"]=s,e.setStatus({code:q.ERROR,message:s})}else e.setStatus({code:q.OK})}}const Y=new K,H=new Set(["tools/call","resources/read","prompts/get"]);class X extends A{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().then(()=>r(),e=>r(e))}catch(e){r(e)}}async processBuffer(){const e=this.buffer.split("\n");this.buffer=e.pop()||"";for(const t of e)await this.processResponseLine(t)}async processResponseLine(e){const r=e.trim();if(!r)return;let n;try{n=JSON.parse(r)}catch{return void t("Non-JSON output filtered:",r.slice(0,100))}if(!n||"2.0"!==n.jsonrpc)return void t("Non-JSON-RPC JSON ignored:",r.slice(0,100));let i=null;if(!n.method&&null!=n.id){const e=this.logger.responseGuardInput(n);e&&H.has(e.method)&&(i=await o((new K).buildPayload(e),process.env.PINTA_GUARD_ENDPOINT??G))}const a="DENY"===i?.decision?JSON.stringify({jsonrpc:"2.0",id:n.id,error:{code:-32e3,message:"Pinta withheld this tool output because it violated an active policy.",data:{reason:"tool_output_denied",phase:"tool_output"}}}):r;try{n.method||null==n.id?this.handleMessage(n):this.logger.onResponse(n,this.direction,i??void 0)}catch(e){t("Response telemetry failed:",e)}this.push(a+"\n"),s(`[${this.direction}] Raw message:`,r)}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&&H.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??G;return t&&"1"!==process.env.PINTA_GUARD_DISABLED?o((new K).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:h(e.method,e.params,void 0,this.sessionContextStore),modelEvidence:v(e,"request")}),t):null}_flush(e){const s=()=>{O&&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))}const t=this.buffer;this.buffer="",this.processResponseLine(t).then(s,t=>e(t))}catch(t){e(t)}}}class Q{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=h(e.method,e.params,void 0,this.sessionContextStore,void 0);y({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:v(e,"request")})}responseGuardInput(e){if(null==e.id)return null;const t=this.pendingRequests.request.get(e.id);return t?{type:"response",method:t.request.method,transport:this.transport,serverUrl:this.serverUrl,requestId:t.request.id,requestDirection:"request",requestTimestamp:t.timestamp,params:t.request.params,responseId:e.id,responseTimestamp:Date.now(),result:e.result,error:e.error,serverInfo:this.sessionContextStore.getServerInfo(),contextMetadata:h(t.request.method,t.request.params,e.result,this.sessionContextStore),modelEvidence:v(t.request,"request",e)}:null}onResponse(e,t="response",s){if(null==e.id)return;const r="response"===t?"request":"response",n=this.pendingRequests[r],i=n.get(e.id),o=Date.now(),a=i?.request.method||e.method||"unknown";let p=this.sessionContextStore.getServerInfo();"request"===r&&"initialize"===i?.request.method&&e.result?.serverInfo&&(p={name:e.result.serverInfo.name,version:e.result.serverInfo.version},this.sessionContextStore.setServerInfo(p));const u=h(a,i?.request.params,e.result,this.sessionContextStore,void 0);i?(clearTimeout(i.timeout),clearTimeout(i.sweep),n.delete(e.id),y({type:i.timedOut?"response":"paired",method:a,transport:this.transport,serverUrl:this.serverUrl,requestId:i.request.id,requestDirection:r,requestTimestamp:i.timedOut?void 0:i.timestamp,params:i.request.params,responseId:e.id,responseTimestamp:o,result:e.result,error:e.error,durationMs:o-i.timestamp,contextMetadata:u,serverInfo:p,guard:i.guard,outputGuard:s,modelEvidence:v(i.request,r,e)})):y({type:"response",method:a,transport:this.transport,serverUrl:this.serverUrl,responseId:e.id,requestDirection:r,responseTimestamp:o,result:e.result,error:e.error,contextMetadata:u,serverInfo:p,outputGuard:s})}onNotification(e){if(!r(e.method))return;const t=h(e.method,e.params,void 0,this.sessionContextStore,void 0);y({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=h(e.request.method,e.request.params,void 0,this.sessionContextStore,void 0);y({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:v(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 ee{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 te{process;logger;isShuttingDown=!1;responseDrained;constructor(e,s,r){const n=new ee;this.logger=new Q(n,{transport:r.transport,serverUrl:r.url}),this.process=T(e,s,{stdio:["pipe","pipe","inherit"],env:{...process.env,MCP_LOGGER_ENABLED:"true"}});const i=new X("request",this.logger,n),o=new X("response",this.logger,n);process.stdin.pipe(i).pipe(this.process.stdin),this.process.stdout.pipe(o).pipe(process.stdout),this.responseDrained=R(o).catch(e=>{t("Response stream failed:",e)}),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.responseDrained,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)).then(()=>this.responseDrained),new Promise(e=>setTimeout(e,5e3))]),await this.cleanup(),process.exit(0))}async cleanup(){this.logger.flushPending(),await async function(){await U.shutdown()}()}}I.name("mcp-logger").description("Wrap an MCP server and forward OTLP traces (OTEL_EXPORTER_OTLP_TRACES_ENDPOINT)").version(b).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=>$.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(z.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=Z.has(e)?"pip":"npm",s=function(e,t){try{if("pip"===t){let t=null;for(const s of["pip3","pip"])try{t=M(`${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=M(`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},U.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){U.setCLIServerInfo(e,t,{})}(s,r),new te(s,r,i)}),I.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pinta-ai/mcp-logger",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Generic MCP-to-OTLP forwarder. Wraps any MCP server and emits OTel traces to OTEL_EXPORTER_OTLP_ENDPOINT.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",