@pinta-ai/mcp-logger 0.1.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 v0.2+ auto-installs this adaptor from the `dev` tag via 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.
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
 
@@ -53,19 +53,69 @@ When `PINTA_GUARD_ENDPOINT` is configured (normally by Pinta Manager), the wrapp
53
53
 
54
54
  `prompts/get` is included because it can retrieve server-controlled text and embedded resources, not just discover available prompts. Initialization, `ping`, notifications and discovery (`tools/list`, `resources/list`, `resources/templates/list`, `prompts/list`) remain ungated.
55
55
 
56
+ The guard is asked about the span itself: the request half of the exchange (`mcp.type: request`, method, params) is POSTed as an OTLP/JSON payload — the same shape the collector receives — so the evaluator judges exactly what will be stored. Requires Pinta Manager 0.1.11 or later; a manager that no longer accepts the body answers `410`, recorded on the span as `pinta.guard.fail_open_reason: refused`.
57
+
56
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.
57
59
 
58
60
  ## What's emitted
59
61
 
60
62
  OTLP HTTP+JSON traces with:
61
63
 
62
- - `service.name` — extracted from the wrapped command (e.g. `@modelcontextprotocol/server-slack`)
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
63
65
  - `service.version` — extracted or resolved via `npm view`
64
- - `mcp.*` attributes — full Bronze-flat encoding of every JSON-RPC message (see [`@pinta-ai/types`](https://www.npmjs.com/package/@pinta-ai/types) for the schema)
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.
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)`
65
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.
66
69
 
67
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).
68
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
+
69
119
  ## Configuration
70
120
 
71
121
  Telemetry uses OTel SDK standard env vars. The optional guard has separate settings.
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){y&&console.error("[mcp-logger]",...e)}function s(...e){x&&console.error("[mcp-logger]",...e)}function n(e){return!w.includes(e)}function i(e,t=2e5){try{const s=JSON.stringify(e);return s.length>t?s.slice(0,t)+"... (truncated)":s}catch{return null}}function r(e,t,s,n,i){const r=function(e,t){const s=e?.getClientInfo();if(s?.name||s?.version)return{name:s.name,version:s.version};const n=e?.getHeaders()||t;if(n){const e=n["user-agent"]||n["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}}(n,i),o=`${r.name||"unknown"}-${r.version||"0"}`,a=q.getOrCreateSessionId(o),p=q.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,i),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),r.name&&(d.clientName=r.name),r.version&&(d.clientVersion=r.version),u&&(d.permissionLevel=u),void 0!==h&&(d.cost=h),d}function o(e,t={}){if(0===e.length)return e;const s=function(e,t){const s=[];for(const n of N){if(n.requireContext&&n.requireContext!==t.context)continue;const i=RegExp(n.regex.source,n.regex.flags);let r;for(;null!==(r=i.exec(e));){const e=r[n.captureGroup??0];if(void 0===e){r.index===i.lastIndex&&i.lastIndex++;continue}const t=r.index,o=r.index+r[0].length,a=t+r[0].indexOf(e),p=a+e.length;s.push({start:t,end:o,replaceStart:a,replaceEnd:p,type:n.type}),r.index===i.lastIndex&&i.lastIndex++}}return s}(e,t);return 0===s.length?e:function(e,t){const s=[...t].sort((e,t)=>t.replaceStart-e.replaceStart);let n=e;for(const e of s)n=n.slice(0,e.replaceStart)+`[PINTA_REDACTED:${e.type}]`+n.slice(e.replaceEnd);return n}(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 n=-1;for(const e of t)e.start<n||(s.push(e),n=e.end);return s}(s))}function a(e){A.build(e)}import{program as p}from"commander";import{NodeSDK as c}from"@opentelemetry/sdk-node";import{OTLPTraceExporter as u}from"@opentelemetry/exporter-trace-otlp-http";import{Resource as h}from"@opentelemetry/resources";import{trace as d,SpanStatusCode as l}from"@opentelemetry/api";import{execSync as m,spawn as f}from"child_process";import{Transform as g}from"stream";import{ulid as v}from"ulid";const I="0.1.3",y="true"===process.env.PINTA_DEBUG||"1"===process.env.PINTA_DEBUG,x="true"===process.env.PINTA_RAW_DEBUG||"1"===process.env.PINTA_RAW_DEBUG,S=I,w=["notifications/initialized"],E=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 _=new class{sdk=null;tracer=null;initialized=!1;cliServerInfo=null;initialize(e){if(this.initialized)return t("Telemetry already initialized, returning existing tracer"),this.tracer;const s=e.serviceName,n=e.serviceVersion||"unknown";t("Initializing telemetry..."),t("Service: "+s),t("Version: "+n);const i={"service.name":s,"service.version":n};this.sdk=new c({resource:new h(i),traceExporter:new u}),this.sdk.start(),this.tracer=d.getTracer("mcp-logger",S),this.initialized=!0,t("Telemetry initialized successfully");const r=async()=>{t("Shutting down telemetry..."),await this.shutdown()};return process.once("SIGTERM",r),process.once("SIGINT",r),this.tracer}getTracer(){return this.tracer}isInitialized(){return this.initialized}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)}}},D=new Set(["npx","node","uvx","python","python3","pip","pipx","deno","bun"]),M=new Set(["uvx","python","python3","pip","pipx"]),T="mcp-logger/"+I,C=new Set(["tools/call","resources/read","prompts/get"]);class R extends g{buffer="";direction;logger;sessionContextStore;constructor(e,t,s){super(),this.direction=e,this.logger=t,this.sessionContextStore=s}_transform(e,s,n){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(()=>n()).catch(e=>n(e))}this.buffer+=e.toString(),this.buffer.length>1e6&&(t("Buffer size exceeded, clearing..."),this.buffer=""),this.processBuffer(!0),n()}catch(e){n(e)}}processBuffer(e){const n=this.buffer.split("\n");this.buffer=n.pop()||"";for(const i of n){const n=i.trim();if(n)try{const i=JSON.parse(n);if(e){if("2.0"!==i.jsonrpc){t("Non-JSON-RPC JSON ignored:",n.slice(0,100));continue}this.push(n+"\n")}s(`[${this.direction}] Raw message:`,n),this.handleMessage(i)}catch{t(e?"Non-JSON output filtered:":"Failed to parse:",n.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,n="\n"){const i=e.trim();if(!i)return;let r;try{r=JSON.parse(i)}catch{return t("Failed to parse:",i.slice(0,100)),void this.push(e+n)}if(r?.method&&C.has(r.method)){const e=r,t=await this.evaluateRequestGuard(e);if("DENY"===t?.decision){const n={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(n)+"\n"),s(`[${this.direction}] DENY:`,i),void this.logger.onRequestDenied(e,t)}this.logger.onRequest(e,t??void 0)}else r&&this.handleMessage(r);this.push(e+n),s(`[${this.direction}] Raw message:`,i)}async evaluateRequestGuard(e){const t=e.params??{},s="tools/call"===e.method,n=s?t.arguments:t,i="string"==typeof n?n:JSON.stringify(n??{}),r=process.env.PINTA_GUARD_ENDPOINT??E;return async function(e,t){if(!t)return null;if("1"===process.env.PINTA_GUARD_DISABLED)return null;const s=Date.now();try{const n=await Promise.race([fetch(t,{method:"POST",headers:{"content-type":"application/json","user-agent":T,"x-pinta-relay-token":process.env.PINTA_RELAY_TOKEN??""},body:JSON.stringify({input:e})}),new Promise((e,t)=>setTimeout(()=>{const e=Error("Guard request timed out");e.name="TimeoutError",t(e)},50))]);if(200!==n.status)return{decision:"ALLOW",reason:null,durationMs:Date.now()-s,failOpenReason:"error"};const i=await n.json();return{decision:i.decision,reason:i.reason,durationMs:i.durationMs??Date.now()-s}}catch(e){const t="TimeoutError"===e.name?"timeout":"error";return{decision:"ALLOW",reason:null,durationMs:Date.now()-s,failOpenReason:t}}}({spanId:(e.id??"unknown")+"",method:e.method,params:t,toolName:s&&"string"==typeof t.name&&t.name?t.name:e.method,toolInput:n,rawTextFields:{toolInput:i}},r)}_flush(e){const s=()=>{y&&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)}}}const q=new class{sessionMap=new Map;sessionCounter=0;conversationMap=new Map;conversationCounter=0;getOrCreateSessionId(e){if(!this.sessionMap.has(e)){const t=v();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}},N=[{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}],A=new class{spanData;input;contextMetadata;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:i(e.error)||"Unknown error";this.spanData["mcp.error.message"]=t}return{...this.spanData}}build(e){const t=_.getTracer();t&&(this.input=e,t.startActiveSpan("mcp."+e.method,e=>{try{this.spanData=this.buildBase(),this.resolveContextMetadata(),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,n]of Object.entries(t))null!=n&&e.setAttribute(s,n)}(e,this.spanData),s("[OTLP] Span data:",JSON.stringify(this.spanData,null,2))}catch{e.setStatus({code:l.ERROR,message:"Error creating span"})}finally{e.end()}}))}buildBase(){return{"ingest.type":"mcp","mcp.type":this.input.type,"mcp.transport":this.input.transport,"mcp.method":this.input.method}}resolveContextMetadata(){this.contextMetadata=this.input.contextMetadata||r(this.input.method,this.input.params,this.input.result,void 0,this.input.headers)}applyRequestFields(){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=i(this.input.params)||"{}";this.spanData["mcp.request.params"]=o(e,{context:"bash"})}if(this.input.headers){const e=i(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=i(this.input.result);e&&(this.spanData["mcp.response.result"]=o(e,{context:"bash"}))}}applyDuration(){void 0!==this.input.durationMs&&(this.spanData["mcp.duration_ms"]=this.input.durationMs)}applyCLIServerInfo(){const e=_.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"]=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:i(this.input.error)||"Unknown error",this.spanData["mcp.error.message"]=t,e.setStatus({code:l.ERROR,message:t})}else e.setStatus({code:l.OK})}};class O{pendingRequests=new Map;TIMEOUT=3e4;sessionContextStore;constructor(e){this.sessionContextStore=e}onRequest(e,t){if(n(e.method)){if(null==e.id){const t=r(e.method,e.params,void 0,this.sessionContextStore,void 0);return void a({type:"notification",method:e.method,transport:"stdio",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}),setTimeout(()=>this.handleTimeout(e.id),this.TIMEOUT)}}onRequestDenied(e,t){if(!n(e.method))return;const s=r(e.method,e.params,void 0,this.sessionContextStore,void 0);a({type:"request",method:e.method,transport:"stdio",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(),n=t?.request.method||e.method||"unknown";let i=this.sessionContextStore.getServerInfo();"initialize"===t?.request.method&&e.result?.serverInfo&&(i={name:e.result.serverInfo.name,version:e.result.serverInfo.version});const o=r(n,t?.request.params,e.result,this.sessionContextStore,void 0);t?(a({type:"paired",method:n,transport:"stdio",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:o,serverInfo:i,guard:t.guard}),this.pendingRequests.delete(e.id)):a({type:"response",method:n,transport:"stdio",responseId:e.id,responseTimestamp:s,result:e.result,error:e.error,contextMetadata:o,serverInfo:i})}onNotification(e){if(!n(e.method))return;const t=r(e.method,e.params,void 0,this.sessionContextStore,void 0);a({type:"notification",method:e.method,transport:"stdio",params:e.params,contextMetadata:t})}handleTimeout(e){const t=this.pendingRequests.get(e);if(t&&Date.now()-t.timestamp>this.TIMEOUT){const s=r(t.request.method,t.request.params,void 0,this.sessionContextStore,void 0);a({type:"request",method:t.request.method,transport:"stdio",requestId:t.request.id,requestTimestamp:t.timestamp,params:t.request.params,contextMetadata:s,guard:t.guard}),this.pendingRequests.delete(e)}}getStats(){return{pending:this.pendingRequests.size}}clear(){this.pendingRequests.clear()}}class k{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 P{process;logger;isShuttingDown=!1;constructor(e,t,s,n){const i=new k;this.logger=new O(i),this.process=f(e,t,{stdio:["pipe","pipe","inherit"],env:{...process.env,MCP_LOGGER_ENABLED:"true"}});const r=new R("request",this.logger,i),o=new R("response",this.logger,i);process.stdin.pipe(r).pipe(this.process.stdin),this.process.stdout.pipe(o).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.clear(),await async function(){await _.shutdown()}()}}p.name("mcp-logger").description("Wrap an MCP server and forward OTLP traces (OTEL_EXPORTER_OTLP_TRACES_ENDPOINT)").version(I).argument("<command...>","MCP server command to wrap").action(t=>{const[s,...n]=t,i=function(e,t){const s=function(e){for(const t of e){if(D.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 n=t.match(/^(@[^@]+\/[^@]+)$/);if(n)return{packageName:n[1],version:"latest"};if(t.match(/^[a-z0-9-]+$/)&&!t.includes("/"))return{packageName:t,version:"latest"}}return null}(t.filter(e=>!e.startsWith("-")));let n,i;if(n=s?.packageName?s.packageName:e.split("/").pop()||e||"mcp-server-unknown",s&&(i=s.version,"latest"===s.version)){const t=M.has(e)?"pip":"npm",n=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 n=t.match(/Available versions:\s*([^\s,]+)/);return n?n[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}}(s.packageName,t);n&&(i=n)}return{name:n,version:i}}(s,n);var r;r={serviceName:i.name,serviceVersion:i.version},_.initialize(r),e("Starting MCP server: "+t.join(" ")),e(`Service: ${i.name} = ${i.version||"unknown"}`),function(e,t){_.setCLIServerInfo(e,t,{})}(s,n),new P(s,n,i.name,i.version)}),p.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 q}from"@opentelemetry/api";import{execSync as _,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.0",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"]),z=new Set(["uvx","python","python3","pip","pipx"]),G="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};return this.input.serverUrl&&(e["mcp.server.url"]=this.input.serverUrl),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:q.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":G,"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=z.has(e)?"pip":"npm",s=function(e,t){try{if("pip"===t){let t=null;for(const s of["pip3","pip"])try{t=_(`${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=_(`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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pinta-ai/mcp-logger",
3
- "version": "0.1.3",
3
+ "version": "0.3.0",
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",
@@ -67,7 +67,6 @@
67
67
  "@opentelemetry/resources": "^1.25.0",
68
68
  "@opentelemetry/sdk-node": "^0.52.0",
69
69
  "@opentelemetry/semantic-conventions": "^1.25.0",
70
- "@pinta-ai/types": "^0.0.6",
71
70
  "commander": "^11.1.0",
72
71
  "ulid": "^3.0.1"
73
72
  },
@@ -77,7 +76,6 @@
77
76
  "@opentelemetry/resources",
78
77
  "@opentelemetry/sdk-node",
79
78
  "@opentelemetry/semantic-conventions",
80
- "@pinta-ai/types",
81
79
  "commander",
82
80
  "ulid"
83
81
  ],
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Pinta AI, Inc.
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
@@ -1,41 +0,0 @@
1
- # @pinta-ai/types
2
-
3
- Single source of truth for shared type definitions across the Pinta platform.
4
-
5
- Defines the data structures exchanged between adapters (mcp-logger, etc.) and the backend (aware-backend).
6
-
7
- ## Install
8
-
9
- ```bash
10
- npm install @pinta-ai/types
11
- ```
12
-
13
- ## Usage
14
-
15
- ```typescript
16
- import type { IngestTypeMap, IngestType, MCPSpan } from '@pinta-ai/types';
17
- ```
18
-
19
- ## Core Types
20
-
21
- | Type | Description |
22
- |------|-------------|
23
- | `IngestTypeMap` | Maps `ingest.type` values to span attribute interfaces |
24
- | `IngestType` | Union of supported ingest types (`'mcp' \| ...`) |
25
- | `MCPSpan` | Span attribute interface for the MCP adapter |
26
-
27
- ## Adding a New Adapter
28
-
29
- 1. Define a span attribute interface in `src/ingest.ts` (e.g., `PintaclawSpan`)
30
- 2. Add mapping to `IngestTypeMap` (e.g., `pintaclaw: PintaclawSpan`)
31
- 3. Export from `src/index.ts`
32
-
33
- ## Conventions
34
-
35
- - Flat key-value structure only (serialized as OTel span attributes)
36
- - Value types: `string | number | boolean | undefined`
37
- - Every span interface must include an `ingest.type` field (discriminator)
38
-
39
- ## License
40
-
41
- [MIT](LICENSE)
@@ -1,81 +0,0 @@
1
- /**
2
- * CDM (Common Data Model) Types
3
- *
4
- * Adaptor 무관한 공통 스키마 정의.
5
- * Silver 레이어에서 사용하는 정규화된 데이터 구조.
6
- *
7
- * 설계 원칙:
8
- * - 분류 차원(category/kind/action/outcome)은 느슨한 의미 분류 (집계용)
9
- * - source.event로 원본 이벤트를 정확히 식별 (디버깅용)
10
- * - 저장소는 enum을 모름 (string으로 저장, enum 추가 시 스키마 변경 없음)
11
- */
12
- export type CDMCategory = 'execution' | 'access' | 'communication' | 'lifecycle';
13
- export type CDMKind = 'tool' | 'resource' | 'prompt' | 'response' | 'agent' | 'session';
14
- export type CDMAction = 'invoke' | 'read' | 'write' | 'list' | 'create' | 'destroy' | 'delegate' | 'start' | 'end' | 'subscribe' | 'unsubscribe' | 'configure' | 'ping' | 'cancel' | 'progress' | 'change';
15
- export type CDMOutcome = 'success' | 'failure' | 'blocked' | 'timeout' | 'unknown';
16
- export interface CDMToolFields {
17
- 'tool.name'?: string;
18
- 'tool.input'?: string;
19
- 'tool.input_size'?: number;
20
- 'tool.output'?: string;
21
- 'tool.output_size'?: number;
22
- }
23
- export interface CDMResourceFields {
24
- 'resource.uri'?: string;
25
- 'resource.type'?: string;
26
- 'resource.size'?: number;
27
- 'resource.mime_type'?: string;
28
- }
29
- export interface CDMMessageFields {
30
- 'message.role'?: string;
31
- 'message.content'?: string;
32
- 'message.content_size'?: number;
33
- 'message.token_count'?: number;
34
- }
35
- export interface CDMDelegateFields {
36
- 'delegate.agent_id'?: string;
37
- 'delegate.agent_type'?: string;
38
- }
39
- export interface CDMErrorFields {
40
- 'error.code'?: string;
41
- 'error.message'?: string;
42
- 'error.reason'?: string;
43
- }
44
- export interface CDMSecurityFields {
45
- 'permission.level'?: string;
46
- }
47
- export interface CDMBillingFields {
48
- 'cost'?: number;
49
- }
50
- export interface CDMHostFields {
51
- 'host.name'?: string;
52
- 'process.owner'?: string;
53
- }
54
- export interface CDMRecord extends CDMToolFields, CDMResourceFields, CDMMessageFields, CDMDelegateFields, CDMErrorFields, CDMSecurityFields, CDMBillingFields, CDMHostFields {
55
- 'record.id': string;
56
- 'record.timestamp': number;
57
- 'record.duration_ms'?: number;
58
- 'record.category': CDMCategory;
59
- 'record.kind': CDMKind;
60
- 'record.action': CDMAction;
61
- 'record.outcome': CDMOutcome;
62
- 'trace.id': string;
63
- 'span.id': string;
64
- 'span.parent_id'?: string;
65
- 'session.id'?: string;
66
- 'conversation.id'?: string;
67
- 'memberIdentity.id'?: string;
68
- 'memberIdentity.email'?: string;
69
- 'client.name'?: string;
70
- 'client.version'?: string;
71
- 'agent.name'?: string;
72
- 'agent.version'?: string;
73
- 'source.adaptor': string;
74
- 'source.event': string;
75
- 'source.raw_id': string;
76
- 'source.transport'?: string;
77
- 'org.id': string;
78
- extra?: string;
79
- }
80
- export type SourceAdaptor = 'mcp-logger' | 'pinta-cc';
81
- //# sourceMappingURL=cdm.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cdm.d.ts","sourceRoot":"","sources":["../src/cdm.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAMH,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,QAAQ,GAAG,eAAe,GAAG,WAAW,CAAC;AAEjF,MAAM,MAAM,OAAO,GAAG,MAAM,GAAG,UAAU,GAAG,QAAQ,GAAG,UAAU,GAAG,OAAO,GAAG,SAAS,CAAC;AAExF,MAAM,MAAM,SAAS,GACjB,QAAQ,GACR,MAAM,GACN,OAAO,GACP,MAAM,GACN,QAAQ,GACR,SAAS,GACT,UAAU,GACV,OAAO,GACP,KAAK,GACL,WAAW,GACX,aAAa,GACb,WAAW,GACX,MAAM,GACN,QAAQ,GACR,UAAU,GACV,QAAQ,CAAC;AAEb,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;AAMnF,MAAM,WAAW,aAAa;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,iBAAiB;IAChC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,gBAAgB;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,iBAAiB;IAChC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,cAAc;IAC7B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,iBAAiB;IAChC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAMD,MAAM,WAAW,SACf,SAAQ,aAAa,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,aAAa;IAEf,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAG9B,iBAAiB,EAAE,WAAW,CAAC;IAC/B,aAAa,EAAE,OAAO,CAAC;IACvB,eAAe,EAAE,SAAS,CAAC;IAC3B,gBAAgB,EAAE,UAAU,CAAC;IAG7B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAG1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAG3B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAGhC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAG1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IAGzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAG5B,QAAQ,EAAE,MAAM,CAAC;IAGjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAMD,MAAM,MAAM,aAAa,GAAG,YAAY,GAAG,UAAU,CAAC"}
@@ -1,14 +0,0 @@
1
- "use strict";
2
- /**
3
- * CDM (Common Data Model) Types
4
- *
5
- * Adaptor 무관한 공통 스키마 정의.
6
- * Silver 레이어에서 사용하는 정규화된 데이터 구조.
7
- *
8
- * 설계 원칙:
9
- * - 분류 차원(category/kind/action/outcome)은 느슨한 의미 분류 (집계용)
10
- * - source.event로 원본 이벤트를 정확히 식별 (디버깅용)
11
- * - 저장소는 enum을 모름 (string으로 저장, enum 추가 시 스키마 변경 없음)
12
- */
13
- Object.defineProperty(exports, "__esModule", { value: true });
14
- //# sourceMappingURL=cdm.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cdm.js","sourceRoot":"","sources":["../src/cdm.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG"}
@@ -1,3 +0,0 @@
1
- export type { IngestTypeMap, IngestType, MCPSpan, MemberIdentity, } from './ingest.js';
2
- export type { CDMCategory, CDMKind, CDMAction, CDMOutcome, CDMToolFields, CDMResourceFields, CDMMessageFields, CDMDelegateFields, CDMErrorFields, CDMSecurityFields, CDMBillingFields, CDMHostFields, CDMRecord, SourceAdaptor, } from './cdm.js';
3
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,aAAa,EACb,UAAU,EACV,OAAO,EACP,cAAc,GACf,MAAM,aAAa,CAAC;AAErB,YAAY,EACV,WAAW,EACX,OAAO,EACP,SAAS,EACT,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,SAAS,EACT,aAAa,GACd,MAAM,UAAU,CAAC"}
@@ -1,3 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- //# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
@@ -1,69 +0,0 @@
1
- /**
2
- * Ingest Types
3
- *
4
- * Adapter → Backend 간 전송되는 로그의 타입 정의.
5
- * 각 adapter는 OTel span으로 로그를 전송하며,
6
- * span attribute의 ingest.type 값으로 로그 종류를 구분한다.
7
- */
8
- /**
9
- * ingest.type → span attribute interface 매핑
10
- * 새 adapter 추가 시 이 맵에 타입을 등록한다.
11
- */
12
- export interface IngestTypeMap {
13
- mcp: MCPSpan;
14
- }
15
- /**
16
- * 지원하는 ingest type union
17
- */
18
- export type IngestType = keyof IngestTypeMap;
19
- /**
20
- * MCP adaptor가 전송하는 raw span attribute 인터페이스 (bronze 형태).
21
- *
22
- * Phase B 이후 adaptor는 semantic 추출(tool.name, resource.*, size 등)을 하지 않는다.
23
- * aware-backend가 Silver 파이프라인에서 `mcp.request.params` / `mcp.response.result`를
24
- * 파싱해 CDM으로 변환한다.
25
- *
26
- * OTel span attributes로 직렬화되므로 flat key-value 구조.
27
- * mcp.type으로 span 종류를 구분:
28
- * - paired: request + response 매칭된 쌍 (duration 포함)
29
- * - request: 응답 없이 타임아웃된 요청
30
- * - response: 요청 없이 수신된 응답
31
- * - notification: ID 없는 단방향 알림
32
- */
33
- export interface MCPSpan {
34
- 'ingest.type': 'mcp';
35
- 'mcp.type': 'paired' | 'request' | 'response' | 'notification';
36
- 'mcp.transport': 'stdio' | 'sse';
37
- 'mcp.method': string;
38
- 'mcp.request.id'?: string;
39
- 'mcp.request.method'?: string;
40
- 'mcp.request.timestamp'?: number;
41
- 'mcp.request.params'?: string;
42
- 'mcp.response.id'?: string;
43
- 'mcp.response.method'?: string;
44
- 'mcp.response.timestamp'?: number;
45
- 'mcp.response.result'?: string;
46
- 'mcp.duration_ms'?: number;
47
- 'mcp.error'?: boolean;
48
- 'mcp.error.code'?: number;
49
- 'mcp.error.message'?: string;
50
- 'mcp.headers'?: string;
51
- 'mcp.cli'?: string;
52
- 'mcp.events'?: string;
53
- 'mcp.events.count'?: number;
54
- 'mcp.sdk.name'?: string;
55
- 'mcp.sdk.version'?: string;
56
- 'mcp.session.id'?: string;
57
- 'mcp.conversation.id'?: string;
58
- 'mcp.user.id'?: string;
59
- 'mcp.client.name'?: string;
60
- 'mcp.client.version'?: string;
61
- 'mcp.permission.level'?: string;
62
- 'mcp.cost'?: number;
63
- }
64
- /** Resource-level member identity attributes */
65
- export interface MemberIdentity {
66
- 'member.identity.id'?: string;
67
- 'member.identity.email'?: string;
68
- }
69
- //# sourceMappingURL=ingest.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ingest.d.ts","sourceRoot":"","sources":["../src/ingest.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,OAAO,CAAC;CACd;AAED;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,aAAa,CAAC;AAM7C;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,OAAO;IAEtB,aAAa,EAAE,KAAK,CAAC;IAGrB,UAAU,EAAE,QAAQ,GAAG,SAAS,GAAG,UAAU,GAAG,cAAc,CAAC;IAC/D,eAAe,EAAE,OAAO,GAAG,KAAK,CAAC;IACjC,YAAY,EAAE,MAAM,CAAC;IAGrB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAG/B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAG3B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAG7B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAG3B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAG/B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAG9B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAMD,gDAAgD;AAChD,MAAM,WAAW,cAAc;IAC7B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,uBAAuB,CAAC,EAAE,MAAM,CAAC;CAClC"}
@@ -1,10 +0,0 @@
1
- "use strict";
2
- /**
3
- * Ingest Types
4
- *
5
- * Adapter → Backend 간 전송되는 로그의 타입 정의.
6
- * 각 adapter는 OTel span으로 로그를 전송하며,
7
- * span attribute의 ingest.type 값으로 로그 종류를 구분한다.
8
- */
9
- Object.defineProperty(exports, "__esModule", { value: true });
10
- //# sourceMappingURL=ingest.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ingest.js","sourceRoot":"","sources":["../src/ingest.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG"}
@@ -1,25 +0,0 @@
1
- {
2
- "name": "@pinta-ai/types",
3
- "version": "0.0.6",
4
- "description": "Shared type definitions for Pinta AI adapters and log processing server",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
7
- "publishConfig": {
8
- "access": "public"
9
- },
10
- "files": [
11
- "dist"
12
- ],
13
- "scripts": {
14
- "build": "tsc",
15
- "clean": "rm -rf dist",
16
- "prepublishOnly": "npm run build"
17
- },
18
- "license": "MIT",
19
- "devDependencies": {
20
- "typescript": "^5.3.0"
21
- },
22
- "engines": {
23
- "node": ">=18"
24
- }
25
- }