@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 +53 -3
- package/dist/cli/index.js +1 -1
- package/package.json +1 -3
- package/node_modules/@pinta-ai/types/LICENSE +0 -21
- package/node_modules/@pinta-ai/types/README.md +0 -41
- package/node_modules/@pinta-ai/types/dist/cdm.d.ts +0 -81
- package/node_modules/@pinta-ai/types/dist/cdm.d.ts.map +0 -1
- package/node_modules/@pinta-ai/types/dist/cdm.js +0 -14
- package/node_modules/@pinta-ai/types/dist/cdm.js.map +0 -1
- package/node_modules/@pinta-ai/types/dist/index.d.ts +0 -3
- package/node_modules/@pinta-ai/types/dist/index.d.ts.map +0 -1
- package/node_modules/@pinta-ai/types/dist/index.js +0 -3
- package/node_modules/@pinta-ai/types/dist/index.js.map +0 -1
- package/node_modules/@pinta-ai/types/dist/ingest.d.ts +0 -69
- package/node_modules/@pinta-ai/types/dist/ingest.d.ts.map +0 -1
- package/node_modules/@pinta-ai/types/dist/ingest.js +0 -10
- package/node_modules/@pinta-ai/types/dist/ingest.js.map +0 -1
- package/node_modules/@pinta-ai/types/package.json +0 -25
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
|
|
|
@@ -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
|
-
- `
|
|
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.
|
|
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 +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
|
-
}
|