@pinta-ai/mcp-logger 0.2.0 → 0.3.1
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 +50 -1
- package/dist/cli/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ npm install -g @pinta-ai/mcp-logger
|
|
|
22
22
|
|
|
23
23
|
### With Pinta Manager (recommended for Pinta users)
|
|
24
24
|
|
|
25
|
-
Pinta Manager
|
|
25
|
+
Pinta Manager installs this adaptor through its catalog and wraps your MCP servers — for Claude Desktop, Cursor, and Claude Code — injecting the OTel env vars. No manual configuration. The manager forwards traces to the Pinta backend with proper authentication and identity attribution. The package uses the stable `latest` channel; no prerelease channel is required.
|
|
26
26
|
|
|
27
27
|
When Manager wraps a server, any legacy `PINTA_API_KEY`/`PINTA_MEMBER_*`/`PINTA_TOKEN` env vars on the wrapped command are stripped and replaced with `OTEL_EXPORTER_OTLP_ENDPOINT`/`OTEL_EXPORTER_OTLP_HEADERS`. Existing users who upgrade to Manager v0.2+ are migrated automatically on the next reconcile.
|
|
28
28
|
|
|
@@ -63,11 +63,59 @@ OTLP HTTP+JSON traces with:
|
|
|
63
63
|
|
|
64
64
|
- `service.name` — extracted from the wrapped command (e.g. `@modelcontextprotocol/server-slack`). When the wrapped command is a stdio↔HTTP bridge (`mcp-remote`, `supergateway`, `mcp-proxy`), the name is the upstream URL's host and path instead, `mcp.transport` is `http` or `sse`, and `mcp.server.url` carries the URL
|
|
65
65
|
- `service.version` — extracted or resolved via `npm view`
|
|
66
|
+
- `telemetry.sdk.name` / `telemetry.sdk.version` — `mcp-logger` and this adaptor's release version, on the shared resource used by both exported traces and guard payloads. These identify the Pinta adaptor, not the wrapped server, calling client, or underlying OpenTelemetry implementation. Scope version alone is not the adaptor identity contract.
|
|
66
67
|
- `mcp.*` attributes — full Bronze-flat encoding of every JSON-RPC message (schema: `src/types/span.ts`). Every span of a session shares one trace id, and the request and response halves of one exchange share a parent span id, so halves emitted apart (a timed-out request, a late response, an in-flight call at shutdown) can be paired downstream on `(trace id, mcp.request.id)`
|
|
67
68
|
- With guard evaluation: `pinta.guard.decision`, `pinta.guard.duration_ms`, and, when present, `pinta.guard.matched_rule` / `pinta.guard.fail_open_reason`. Denied requests emit a request span; forwarded requests retain the decision on their paired request/response span.
|
|
68
69
|
|
|
69
70
|
Identity attribution (`member.identity.*`) is **not** added by this adaptor — it is the relay/collector's responsibility (Pinta Manager attaches on forward; OSS users attach via their own pipeline).
|
|
70
71
|
|
|
72
|
+
### Model visibility: explicit evidence only
|
|
73
|
+
|
|
74
|
+
**Ordinary MCP `tools/call` traffic does not identify the host's LLM.** This adaptor does not discover a model from Claude/Cursor names, client versions, the wrapped server package, tool arguments, or model preferences. Missing evidence stays missing; it is not filled with a guessed model or an `"unknown"` model entry.
|
|
75
|
+
|
|
76
|
+
The following optional span attributes describe only the exchange carrying the evidence:
|
|
77
|
+
|
|
78
|
+
| Attribute | Meaning |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `mcp.model` | Exact string model ID with boundary whitespace trimmed; otherwise unchanged, including case |
|
|
81
|
+
| `mcp.model_source` | `sampling_response` or `pinta_request_meta`, as described below |
|
|
82
|
+
| `mcp.model_provider` | Optional, explicitly declared provider string; never inferred from a model prefix or client name |
|
|
83
|
+
|
|
84
|
+
Supported evidence:
|
|
85
|
+
|
|
86
|
+
1. **MCP sampling result** — A server-to-client [`sampling/createMessage`](https://modelcontextprotocol.io/specification/2025-11-25/client/sampling) request followed by its matching successful client response with `result.model`. Only that sampling span gets `mcp.model_source=sampling_response`. Optional provider evidence comes from `result._meta["sh.pinta/model-provider"]`. Sampling preferences/hints are not the selected model. A sampling result is **not** evidence of the model that initiated an enclosing tool call, subsequent tool calls, another subagent, or the whole session.
|
|
87
|
+
2. **Opt-in caller declaration on a tool request** — A client may supply `params._meta["sh.pinta/model"]` on each individual `tools/call` request, with optional `params._meta["sh.pinta/model-provider"]`. This produces `mcp.model_source=pinta_request_meta`. This is an explicit Pinta extension using MCP's [`_meta` namespace mechanism](https://modelcontextprotocol.io/specification/2025-11-25/basic#_meta), **not a standard MCP field or something existing clients automatically send**. The adaptor only reads it; it never adds metadata to protocol messages.
|
|
88
|
+
|
|
89
|
+
Example of an explicitly attributed tool request:
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"jsonrpc": "2.0",
|
|
94
|
+
"id": 41,
|
|
95
|
+
"method": "tools/call",
|
|
96
|
+
"params": {
|
|
97
|
+
"name": "query",
|
|
98
|
+
"arguments": { "model": "Customer" },
|
|
99
|
+
"_meta": {
|
|
100
|
+
"sh.pinta/model": "provider/exact-model-id",
|
|
101
|
+
"sh.pinta/model-provider": "declared-provider"
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Here `arguments.model` is application data and is ignored for model attribution. Generic `params.model`, generic `_meta.model`, metadata nested inside tool arguments, and model-shaped tool results are also ignored. Empty strings, `unknown` (case-insensitive), arrays, objects, numbers, and provider-only declarations do not produce a model. After trimming, any string beginning with `{` or `[` is also omitted, including malformed or truncated object/array representations.
|
|
108
|
+
|
|
109
|
+
No model is cached as a connection/session default. Initialization metadata and sampling capabilities do not establish one. Requests are correlated independently by connection, direction, and exact JSON-RPC ID type, so simultaneous numeric/string IDs and IDs reused by opposite peers do not mix evidence. A request span is emitted after 30 seconds; correlation is retained for the existing bounded 10-minute late-response window. A late sampling response can identify the model of that same exchange, but unmatched or expired results cannot. An explicit tool-request declaration can still appear on a denied, failed, timed-out, or shutdown request span: it describes the caller's declaration, not proof that the tool or a model invocation succeeded.
|
|
110
|
+
|
|
111
|
+
The same optional model attributes are included in the pre-forward OTLP guard payload when a tool request declares them. The envelope remains `{resourceSpans}` for Pinta Manager **0.1.11+**; guarded methods, DENY handling, and ALLOW/REVIEW/fail-open behavior are unchanged.
|
|
112
|
+
|
|
113
|
+
### Caller identity and transport limits
|
|
114
|
+
|
|
115
|
+
`initialize.params.clientInfo` supplies `mcp.client.name` / `mcp.client.version` for that connection. They identify the declared MCP client, not its model. `service.name` / `service.version` describe the **wrapped MCP server package**, and `mcp.sdk.*` describes its protocol `serverInfo`; neither is the calling Agent. Without client identity, the underlying Agent remains unknown. Member/user attribution still belongs to the relay/collector.
|
|
116
|
+
|
|
117
|
+
The executable observes both directions of the wrapped process's stdio connection. Recognized `mcp-remote`, `supergateway`, and `mcp-proxy` bridges retain their upstream **HTTP/Streamable HTTP or SSE** transport, URL, and server identity; the same model evidence extraction applies to all three `mcp.transport` values (`stdio`, `http`, `sse`). There is no separate native HTTP/SSE listener or public SDK instrumentation API. A bridge is observed at its stdio boundary, so identity hidden by that bridge cannot be recovered. Full model visibility for normal tools therefore requires per-request client declarations or separate host/SDK instrumentation, not just installing this adaptor.
|
|
118
|
+
|
|
71
119
|
## Configuration
|
|
72
120
|
|
|
73
121
|
Telemetry uses OTel SDK standard env vars. The optional guard has separate settings.
|
|
@@ -80,6 +128,7 @@ Telemetry uses OTel SDK standard env vars. The optional guard has separate setti
|
|
|
80
128
|
| `PINTA_GUARD_DISABLED` | Set to `1` to skip guard evaluation | unset |
|
|
81
129
|
| `PINTA_DEBUG` | mcp-logger debug logs to stderr | off |
|
|
82
130
|
| `PINTA_RAW_DEBUG` | verbose logs (raw OTLP, MCP messages) | off |
|
|
131
|
+
| `PINTA_MCP_CONFIG_KEY` | The wrapped server's name under `mcpServers` in the client's MCP config (e.g. `notion`). Copied, trimmed, onto every span as `mcp.server.config_key` so the server can be matched to the name hook-based adaptors report. Pinta Manager sets it when it wraps a server; you normally never set it yourself. | unset (attribute omitted) |
|
|
83
132
|
|
|
84
133
|
See the [OpenTelemetry SDK docs](https://opentelemetry.io/docs/specs/otel/protocol/exporter/) for the full env spec.
|
|
85
134
|
|
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){R&&console.error("[mcp-logger]",...e)}function s(...e){T&&console.error("[mcp-logger]",...e)}function r(e){return!A.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 _("sha256").update(e).digest("hex").slice(0,32)}function p(e,t){return _("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 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=`${i.name||"unknown"}-${i.version||"0"}`,a=L.getOrCreateSessionId(o),p=L.getOrCreateConversationId(a,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),u=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),d={};return a&&(d.sessionId=a),p&&(d.conversationId=p),c&&(d.userId=c),i.name&&(d.clientName=i.name),i.version&&(d.clientVersion=i.version),u&&(d.permissionLevel=u),void 0!==h&&(d.cost=h),d}function d(e,t={}){if(0===e.length)return e;const s=function(e,t){const s=[];for(const r of $){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 l(e){G.build(e)}import{program as m}from"commander";import{NodeSDK as f}from"@opentelemetry/sdk-node";import{OTLPTraceExporter as v}from"@opentelemetry/exporter-trace-otlp-http";import{Resource as g}from"@opentelemetry/resources";import{trace as I,SpanStatusCode as y,context as x,TraceFlags as S}from"@opentelemetry/api";import{execSync as w,spawn as D}from"child_process";import{Transform as E}from"stream";import{createHash as _}from"node:crypto";import{ulid as q}from"ulid";const M="0.2.0",R="true"===process.env.PINTA_DEBUG||"1"===process.env.PINTA_DEBUG,T="true"===process.env.PINTA_RAW_DEBUG||"1"===process.env.PINTA_RAW_DEBUG,C=M,A=["notifications/initialized"],N=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 b=new class{sdk=null;tracer=null;initialized=!1;cliServerInfo=null;resourceAttrs={};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={"service.name":s,"service.version":r};this.resourceAttrs=n,this.sdk=new f({resource:new g(n),traceExporter:new v}),this.sdk.start(),this.tracer=I.getTracer("mcp-logger",C),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)}}},O=new Set(["mcp-remote","supergateway","mcp-proxy"]),k=new Set(["npx","node","uvx","python","python3","pip","pipx","deno","bun"]),P=new Set(["uvx","python","python3","pip","pipx"]),U="mcp-logger/"+M,L=new class{sessionMap=new Map;sessionCounter=0;conversationMap=new Map;conversationCounter=0;getOrCreateSessionId(e){if(!this.sessionMap.has(e)){const t=q();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}},$=[{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 z{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:b.getResourceAttributes(),scope:{name:"mcp-logger",version:M},...s?{traceId:a(s)}:{},...s&&void 0!==e.requestId?{spanId:p(s,e.requestId)}:{},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.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=b.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.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:y.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};return this.input.serverUrl&&(e["mcp.server.url"]=this.input.serverUrl),e}parentContext(){const e=this.contextMetadata.sessionId;if(!e)return x.active();const t=this.input.requestId??this.input.responseId;return I.setSpanContext(x.active(),{traceId:a(e),spanId:p(e,void 0===t?"session":t),traceFlags:S.SAMPLED,isRemote:!0})}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){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"]=d(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"]=d(e,{context:"bash"}))}}applyDuration(){void 0!==this.input.durationMs&&(this.spanData["mcp.duration_ms"]=this.input.durationMs)}applyCLIServerInfo(){const e=b.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))}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:y.ERROR,message:t})}else e.setStatus({code:y.OK})}}const G=new z,Z=new Set(["tools/call","resources/read","prompts/get"]);class V extends E{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))}}}isValidInfo(e){return!(!e?.name||!e?.version)}handleMessage(e){if("request"===this.direction&&"initialize"===e.method&&e.params?.clientInfo&&!this.isValidInfo(this.sessionContextStore.getClientInfo())){const t=e.params;this.sessionContextStore.setClientInfo({name:t.clientInfo?.name,version:t.clientInfo?.version})}if("response"===this.direction&&void 0!==e.id&&!e.method){const t=e;if(t.result?.serverInfo&&!this.isValidInfo(this.sessionContextStore.getServerInfo())){const e=t.result.serverInfo;this.sessionContextStore.setServerInfo({name:e.name,version:e.version})}}"request"===this.direction&&e.method?this.logger.onRequest(e):"response"===this.direction&&(void 0===e.id||e.method?e.method&&null==e.id&&this.logger.onNotification(e):this.logger.onResponse(e))}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&&Z.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??N;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":U,"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 z).buildPayload({type:"request",method:e.method,transport:this.logger.transport,serverUrl:this.logger.serverUrl,requestId:e.id??void 0,requestTimestamp:Date.now(),params:e.params,serverInfo:this.sessionContextStore.getServerInfo()}),t):null}_flush(e){const s=()=>{R&&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 B{pendingRequests=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){if(!r(e.method))return;if(null==e.id){const t=h(e.method,e.params,void 0,this.sessionContextStore,void 0);return void l({type:"notification",method:e.method,transport:this.transport,serverUrl:this.serverUrl,params:e.params,contextMetadata:t})}"initialize"===e.method&&e.params?.clientInfo&&this.sessionContextStore.setClientInfo({name:e.params.clientInfo.name,version:e.params.clientInfo.version}),this.pendingRequests.set(e.id,{request:e,timestamp:Date.now(),guard:t});const s=setTimeout(()=>this.handleTimeout(e.id),this.TIMEOUT);s.unref?.()}onRequestDenied(e,t){if(!r(e.method))return;const s=h(e.method,e.params,void 0,this.sessionContextStore,void 0);l({type:"request",method:e.method,transport:this.transport,serverUrl:this.serverUrl,requestId:e.id??void 0,requestTimestamp:Date.now(),params:e.params,contextMetadata:s,guard:t})}onResponse(e){if(null==e.id)return;const t=this.pendingRequests.get(e.id),s=Date.now(),r=t?.request.method||e.method||"unknown";let n=this.sessionContextStore.getServerInfo();"initialize"===t?.request.method&&e.result?.serverInfo&&(n={name:e.result.serverInfo.name,version:e.result.serverInfo.version});const i=h(r,t?.request.params,e.result,this.sessionContextStore,void 0);t?.timedOut?(l({type:"response",method:r,transport:this.transport,serverUrl:this.serverUrl,requestId:t.request.id,responseId:e.id,responseTimestamp:s,result:e.result,error:e.error,durationMs:s-t.timestamp,contextMetadata:i,serverInfo:n,guard:t.guard}),this.pendingRequests.delete(e.id)):t?(l({type:"paired",method:r,transport:this.transport,serverUrl:this.serverUrl,requestId:t.request.id,requestTimestamp:t.timestamp,params:t.request.params,responseId:e.id,responseTimestamp:s,result:e.result,error:e.error,durationMs:s-t.timestamp,contextMetadata:i,serverInfo:n,guard:t.guard}),this.pendingRequests.delete(e.id)):l({type:"response",method:r,transport:this.transport,serverUrl:this.serverUrl,responseId:e.id,responseTimestamp:s,result:e.result,error:e.error,contextMetadata:i,serverInfo:n})}onNotification(e){if(!r(e.method))return;const t=h(e.method,e.params,void 0,this.sessionContextStore,void 0);l({type:"notification",method:e.method,transport:this.transport,serverUrl:this.serverUrl,params:e.params,contextMetadata:t})}handleTimeout(e){const t=this.pendingRequests.get(e);if(t&&!t.timedOut&&Date.now()-t.timestamp>=this.TIMEOUT){this.emitRequestSpan(t),t.timedOut=!0;const s=setTimeout(()=>{this.pendingRequests.get(e)?.timedOut&&this.pendingRequests.delete(e)},6e5);s.unref?.()}}emitRequestSpan(e){const t=h(e.request.method,e.request.params,void 0,this.sessionContextStore,void 0);l({type:"request",method:e.request.method,transport:this.transport,serverUrl:this.serverUrl,requestId:e.request.id,requestTimestamp:e.timestamp,params:e.request.params,contextMetadata:t,guard:e.guard})}flushPending(){for(const e of this.pendingRequests.values())e.timedOut||this.emitRequestSpan(e);this.pendingRequests.clear()}getStats(){return{pending:this.pendingRequests.size}}clear(){this.pendingRequests.clear()}}class J{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 j{process;logger;isShuttingDown=!1;constructor(e,t,s){const r=new J;this.logger=new B(r,{transport:s.transport,serverUrl:s.url}),this.process=D(e,t,{stdio:["pipe","pipe","inherit"],env:{...process.env,MCP_LOGGER_ENABLED:"true"}});const n=new V("request",this.logger,r),i=new V("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 b.shutdown()}()}}m.name("mcp-logger").description("Wrap an MCP server and forward OTLP traces (OTEL_EXPORTER_OTLP_TRACES_ENDPOINT)").version(M).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=>O.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(k.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=P.has(e)?"pip":"npm",s=function(e,t){try{if("pip"===t){let t=null;for(const s of["pip3","pip"])try{t=w(`${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=w(`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},b.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){b.setCLIServerInfo(e,t,{})}(s,r),new j(s,r,i)}),m.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 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();
|
package/package.json
CHANGED