@awarecorp/mcp-logger 0.0.2-dev.3 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,20 +1,26 @@
1
1
  # @awarecorp/mcp-logger
2
2
 
3
- **통합 MCP Observability 솔루션** - 하나의 패키지로 SDK와 CLI를 모두 제공합니다.
3
+ **Unified MCP Observability Solution** - Monitor your Model Context Protocol servers with zero configuration.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@awarecorp/mcp-logger)](https://www.npmjs.com/package/@awarecorp/mcp-logger)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ [Aware Corp](https://awarecorp.io/)
4
9
 
5
10
  ---
6
11
 
7
- ## 🎯 두 가지 사용 방법
12
+ ## 🎯 Overview
13
+
14
+ `@awarecorp/mcp-logger` provides comprehensive observability for MCP servers in two ways:
8
15
 
9
- ### 1️⃣ **SDK**: 코드에 직접 통합
10
- 당신이 MCP 서버를 개발하고 있다면, SDK를 사용하세요.
16
+ - **SDK**: Direct integration for custom MCP servers
17
+ - **CLI**: Zero-code wrapper for existing MCP packages
11
18
 
12
- ### 2️⃣ **CLI**: 코드 수정 없이 래핑
13
- 이미 만들어진 MCP 서버(npm 패키지)를 사용한다면, CLI를 사용하세요.
19
+ All telemetry data is sent to OpenTelemetry-compatible endpoints with automatic request-response pairing.
14
20
 
15
21
  ---
16
22
 
17
- ## 📦 설치
23
+ ## 📦 Installation
18
24
 
19
25
  ```bash
20
26
  npm install @awarecorp/mcp-logger
@@ -22,296 +28,197 @@ npm install @awarecorp/mcp-logger
22
28
 
23
29
  ---
24
30
 
25
- ## 1️⃣ SDK 사용법
31
+ ## 🚀 Quick Start
26
32
 
27
- ### TypeScript
33
+ ### SDK Mode
34
+
35
+ Instrument your MCP server with one line of code:
28
36
 
29
37
  ```typescript
30
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
31
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
32
- import { initMCPLogger } from '@awarecorp/mcp-logger';
33
-
34
- const server = new Server(
35
- { name: 'my-server', version: '1.0.0' },
36
- { capabilities: { tools: {} } }
37
- );
38
-
39
- // 🔥 자동 계측 활성화 (이것만 추가하면 됩니다!)
40
- initMCPLogger(server, {
41
- apiKey: process.env.AWARE_API_KEY!,
42
- serviceName: 'my-awesome-mcp-server',
43
- debug: true, // 디버그 로그 활성화 (선택사항)
44
- });
38
+ import { trace } from '@awarecorp/mcp-logger';
45
39
 
46
- // 평소처럼 핸들러 등록
47
- server.setRequestHandler('tools/list', async () => ({
48
- tools: [
49
- { name: 'read_file', description: 'Read a file' },
50
- { name: 'write_file', description: 'Write to a file' },
51
- ],
52
- }));
53
-
54
- server.setRequestHandler('tools/call', async (request) => {
55
- const { name, arguments: args } = request.params;
56
- // 이 핸들러는 자동으로 로깅됩니다!
57
- return { result: `Called ${name}` };
40
+ trace(server, {
41
+ apiKey: 'YOUR_API_KEY',
42
+ serviceName: 'my-mcp-server'
58
43
  });
44
+ ```
59
45
 
60
- // stdio transport 시작
61
- const transport = new StdioServerTransport();
62
- await server.connect(transport);
46
+ ### CLI Mode
47
+
48
+ Wrap any existing MCP server without code changes:
49
+
50
+ ```bash
51
+ npx @awarecorp/mcp-logger \
52
+ -k YOUR_API_KEY \
53
+ -s my-server \
54
+ npx your-mcp-server
63
55
  ```
64
56
 
65
- ### JavaScript (CommonJS)
57
+ ---
66
58
 
67
- ```javascript
68
- const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
69
- const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
70
- const { initMCPLogger } = require('@awarecorp/mcp-logger');
59
+ ## 📋 Features
71
60
 
72
- const server = new Server(
73
- { name: 'my-server', version: '1.0.0' },
74
- { capabilities: { tools: {} } }
75
- );
61
+ ### Core Capabilities
76
62
 
77
- // 🔥 자동 계측 활성화
78
- initMCPLogger(server, {
79
- apiKey: process.env.AWARE_API_KEY,
80
- serviceName: 'my-mcp-server',
81
- });
63
+ - **Automatic Request-Response Pairing**: Single span per request-response cycle
64
+ - **Selective Method Tracking**: Only tracks `tools/call` and `tools/list` for efficiency
65
+ - **Custom Event Logging**: Add context-aware logs within request handlers
66
+ - **Transport Support**: stdio and SSE (Server-Sent Events)
67
+ - **Zero Configuration**: Works out of the box with sensible defaults
82
68
 
83
- server.setRequestHandler('tools/list', async () => ({
84
- tools: [{ name: 'test', description: 'Test tool' }],
85
- }));
69
+ ### Telemetry Data
86
70
 
87
- const transport = new StdioServerTransport();
88
- server.connect(transport);
89
- ```
71
+ All spans include:
72
+
73
+ - Request/response timestamps and payloads
74
+ - Method name and parameters
75
+ - Tool names and arguments (for `tools/call`)
76
+ - Custom events and logs
77
+ - Error details (if any)
78
+ - Duration metrics
79
+ - CLI server information (command, args, env)
80
+
81
+ ### Data Format
82
+
83
+ Optimized for Elasticsearch with flat structure:
84
+ - `mcp.method`: Method name
85
+ - `mcp.source`: `sdk` or `cli`
86
+ - `mcp.transport`: `stdio` or `sse`
87
+ - `mcp.duration_ms`: Request duration
88
+ - `mcp.tool.name`: Tool name (tools/call only)
89
+ - `mcp.events`: Custom log entries (JSON array)
90
+ - `mcp.cli.server`: CLI execution info (JSON object)
90
91
 
91
- ### SDK API
92
+ ---
93
+
94
+ ## 🔧 SDK API
92
95
 
93
- #### `initMCPLogger(server, options)`
96
+ ### `trace(server, options)`
94
97
 
95
- MCP 서버에 자동 계측을 추가합니다.
98
+ Enable automatic instrumentation on an MCP server.
96
99
 
97
100
  **Parameters:**
98
- - `server` (`MCPServer`): MCP Server 인스턴스
99
- - `options` (`SDKInitOptions`):
100
- - `apiKey` (string, 필수): Aware API 키
101
- - `serviceName` (string, 옵션): 서비스 이름 (기본값: 자동 생성)
102
- - `debug` (boolean, 옵션): 디버그 로그 활성화 (기본값: false)
103
- - `endpoint` (string, 옵션): 커스텀 OTLP 엔드포인트
101
+ - `server`: MCP Server instance
102
+ - `options.apiKey`: API key for authentication (required)
103
+ - `options.serviceName`: Service identifier (optional, auto-generated if omitted)
104
+ - `options.endpoint`: Custom OTLP endpoint (optional)
105
+ - `options.debug`: Enable debug logging (optional, default: false)
104
106
 
105
107
  **Returns:** `void`
106
108
 
107
- **Throws:**
108
- - `Error`: apiKey가 제공되지 않은 경우
109
- - `Error`: server 인스턴스가 제공되지 않은 경우
109
+ ### `trace.addLog(entry)`
110
110
 
111
- ---
111
+ Add custom log entries within request handlers.
112
112
 
113
- ## 2️⃣ CLI 사용법
113
+ **Parameters:**
114
+ - `entry.level`: Log level (`info` | `warn` | `error`)
115
+ - `entry.message`: Log message (required)
116
+ - `entry.metadata`: Additional data (optional)
117
+ - `entry.timestamp`: Unix timestamp in milliseconds (optional, auto-generated)
114
118
 
115
- ### 기본 사용법
119
+ **Returns:** `void`
116
120
 
117
- ```bash
118
- npx @awarecorp/mcp-logger \
119
- --api-key YOUR_API_KEY \
120
- -- \
121
- npx @modelcontextprotocol/server-filesystem /path/to/dir
122
- ```
121
+ ### `trace.shutdown()`
123
122
 
124
- `--` 뒤에 원래 사용하던 MCP 서버 명령어를 그대로 입력하면 됩니다.
123
+ Gracefully shutdown telemetry and flush pending data.
125
124
 
126
- ### 전역 설치 (선택사항)
125
+ **Returns:** `Promise<void>`
127
126
 
128
- ```bash
129
- npm install -g @awarecorp/mcp-logger
130
- ```
127
+ ---
128
+
129
+ ## 🖥️ CLI Usage
130
+
131
+ ### Basic Syntax
131
132
 
132
- 사용:
133
133
  ```bash
134
- mcp-logger --api-key YOUR_KEY -- npx server-filesystem /path
134
+ mcp-logger [options] <command...>
135
135
  ```
136
136
 
137
- ### Claude Desktop 설정
137
+ ### Options
138
138
 
139
- #### Before (관측성 없음)
140
- ```json
141
- {
142
- "mcpServers": {
143
- "filesystem": {
144
- "command": "npx",
145
- "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/username/Documents"]
146
- }
147
- }
148
- }
139
+ - `-k, --api-key <key>`: API key (required)
140
+ - `-s, --service-name <name>`: Service name (optional)
141
+ - `-e, --endpoint <url>`: Custom OTLP endpoint (optional)
142
+
143
+ ### Examples
144
+
145
+ **Wrap an npx command:**
146
+ ```bash
147
+ mcp-logger -k API_KEY -s filesystem npx -y @modelcontextprotocol/server-filesystem /path
149
148
  ```
150
149
 
151
- #### After (관측성 추가) ✨
150
+ **Use with Claude Desktop:**
152
151
  ```json
153
152
  {
154
153
  "mcpServers": {
155
- "filesystem": {
154
+ "my-server": {
156
155
  "command": "npx",
157
156
  "args": [
158
157
  "-y",
159
158
  "@awarecorp/mcp-logger",
160
- "--api-key", "your-aware-api-key",
161
- "--service-name", "my-filesystem-server",
159
+ "-k",
160
+ "YOUR_API_KEY",
161
+ "-s",
162
+ "my-server",
162
163
  "--",
163
- "@modelcontextprotocol/server-filesystem",
164
- "/Users/username/Documents"
164
+ "npx",
165
+ "-y",
166
+ "your-mcp-server"
165
167
  ]
166
168
  }
167
169
  }
168
170
  }
169
171
  ```
170
172
 
171
- **이게 전부입니다!** 코드 수정 없이 모든 요청/응답이 Aware 플랫폼에 기록됩니다.
172
-
173
- ### CLI 옵션
174
-
175
- #### 필수 옵션
176
- - `--api-key <key>` 또는 `-k <key>`: Aware API 키 (필수)
177
-
178
- #### 선택 옵션
179
- - `--service-name <name>` 또는 `-s <name>`: 서비스 식별 이름 (기본값: 자동 생성)
180
- - `--debug` 또는 `-d`: 디버그 로그 활성화
181
- - `--endpoint <url>` 또는 `-e <url>`: 커스텀 OTLP 엔드포인트
182
-
183
- ### CLI 사용 예시
184
-
185
- #### 파일시스템 서버
186
- ```bash
187
- npx @awarecorp/mcp-logger \
188
- --api-key abc123 \
189
- --service-name my-filesystem \
190
- -- \
191
- npx -y @modelcontextprotocol/server-filesystem /Users/username/Documents
192
- ```
193
-
194
- #### GitHub 서버
195
- ```bash
196
- npx @awarecorp/mcp-logger \
197
- -k abc123 \
198
- -s github-integration \
199
- -- \
200
- npx -y @modelcontextprotocol/server-github
201
- ```
202
-
203
- #### Brave Search 서버 (디버그 모드)
204
- ```bash
205
- npx @awarecorp/mcp-logger \
206
- --api-key abc123 \
207
- --service-name brave-search \
208
- --debug \
209
- -- \
210
- npx -y @modelcontextprotocol/server-brave-search
211
- ```
212
-
213
173
  ---
214
174
 
215
- ## 🏗️ 아키텍처
175
+ ## 🏗️ How It Works
216
176
 
217
- 패키지는 세 가지 레이어로 구성되어 있습니다:
177
+ ### Request-Response Pairing
218
178
 
219
- ```
220
- @awarecorp/mcp-logger
221
- ├── SDK (공개 API)
222
- │ ├── initMCPLogger() - MCP 서버 계측
223
- │ └── shutdown() - 텔레메트리 종료
224
-
225
- ├── CLI (바이너리만 접근 가능)
226
- │ └── mcp-logger - CLI 래퍼
227
-
228
- └── Shared (내부 구현)
229
- ├── telemetry - OpenTelemetry 초기화
230
- ├── span-utils - Span 유틸리티
231
- └── config - 공통 설정
232
- ```
179
+ Creates **one span per request-response pair** for complete transaction context:
233
180
 
234
- ### 중요한 설계 원칙
181
+ 1. Request arrives → stored in pending map
182
+ 2. Response arrives → matched by ID, single span created
183
+ 3. Timeout (30s) → pending requests cleared
235
184
 
236
- 1. **SDK만 공개 API로 노출**: `import`로는 SDK만 접근 가능
237
- 2. **CLI는 바이너리로만 접근**: `npx` 또는 `mcp-logger` 명령어로만 사용
238
- 3. **공통 코드 재사용**: OpenTelemetry 로직은 shared 레이어에서 통합 관리
239
- 4. **타입 안전성**: CLI는 타입 정의가 노출되지 않음
185
+ ### Tracked Methods
240
186
 
241
- ---
187
+ Only tracks business-critical methods:
188
+ - `tools/call` - Tool executions
189
+ - `tools/list` - Tool discovery
242
190
 
243
- ## 🔧 개발
244
-
245
- ### 빌드
246
-
247
- ```bash
248
- npm run build
249
- ```
250
-
251
- ### 로컬 테스트
252
-
253
- #### SDK 테스트
254
- ```typescript
255
- // test.ts
256
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
257
- import { initMCPLogger } from './src/index.js';
258
-
259
- const server = new Server({ name: 'test', version: '1.0.0' }, { capabilities: {} });
260
- initMCPLogger(server, { apiKey: 'test', debug: true });
261
- ```
262
-
263
- #### CLI 테스트
264
- ```bash
265
- npm run build
266
- node bin/mcp-logger.js --help
267
- ```
191
+ Protocol methods (`initialize`, `ping`, etc.) are ignored.
268
192
 
269
193
  ---
270
194
 
271
- ## 📝 라이선스
195
+ ## 📊 Telemetry Data Structure
272
196
 
273
- MIT
197
+ Each span includes:
198
+ - Request/response timestamps and payloads
199
+ - Method name and parameters
200
+ - Tool names and arguments (for `tools/call`)
201
+ - Custom events and logs
202
+ - Error details and duration metrics
203
+ - CLI server information (command, args, env)
274
204
 
275
205
  ---
276
206
 
277
- ## 🤝 기여
207
+ ## 🤝 Contributing
278
208
 
279
- 이슈와 PR은 언제나 환영합니다!
280
-
281
- 자세한 개발 가이드는 [DEVELOPMENT.md](./DEVELOPMENT.md)를 참조하세요.
209
+ For development setup and guidelines, visit [GitHub Repository](https://github.com/awarecorp/mcp-logger).
282
210
 
283
211
  ---
284
212
 
285
- ## 📦 배포 (Maintainers Only)
286
-
287
- **권장:** GitHub Actions를 사용한 자동 배포
288
-
289
- ### 자동 배포 (GitHub Actions) ⭐
290
- ```bash
291
- # Dev 버전 (develop 브랜치)
292
- git checkout develop
293
- npm run version:dev
294
- git push origin develop --tags
295
- ```
296
-
297
- 자세한 가이드:
298
- - [GITHUB_SETUP.md](./GITHUB_SETUP.md) - Repository 초기 설정
299
- - [GITHUB_ACTIONS.md](./GITHUB_ACTIONS.md) - 자동 배포 사용법
300
-
301
- ### 수동 배포 (로컬)
302
- ```bash
303
- # Dev 버전
304
- npm run version:dev
305
- npm run publish:dev
306
- ```
307
-
308
- **참고:** Stable 버전은 아직 지원하지 않습니다.
213
+ ## 📄 License
309
214
 
310
- 자세한 개발 가이드: [DEVELOPMENT.md](./DEVELOPMENT.md)
215
+ MIT © [Aware Corp](https://awarecorp.io/)
311
216
 
312
217
  ---
313
218
 
314
- ## 📞 지원
219
+ ## 🔗 Links
315
220
 
316
- - 문서: [https://aware.mcypher.com/docs](https://aware.mcypher.com/docs)
317
- - 이슈: [GitHub Issues](https://github.com/your-org/mcp-logger/issues)
221
+ - [Aware Corp](https://awarecorp.io/)
222
+ - [npm Package](https://www.npmjs.com/package/@awarecorp/mcp-logger)
223
+ - [GitHub Repository](https://github.com/awarecorp/mcp-logger)
224
+ - [Model Context Protocol](https://modelcontextprotocol.io/)
package/bin/mcp-logger.js CHANGED
@@ -6,4 +6,4 @@
6
6
  * 컴파일된 CLI를 실행합니다.
7
7
  */
8
8
 
9
- import '../dist/cli/cli.js';
9
+ import '../dist/cli/main.js';
@@ -1 +1 @@
1
- import{Transform as e}from"stream";import"@opentelemetry/api";import"@opentelemetry/sdk-node";import"@opentelemetry/exporter-trace-otlp-http";import"@opentelemetry/resources";class r extends e{buffer="";direction;debug;constructor(e,r=!1){super(),this.direction=e,this.debug=r}_transform(e,r,t){try{this.push(e),this.buffer+=e.toString(),this.buffer.length>1e6&&(this.debug&&console.error("[MCP Logger CLI] Buffer size exceeded, clearing..."),this.buffer=""),this.tryParseMessages(),t()}catch(e){t(e)}}tryParseMessages(){const e=this.buffer.split("\n");this.buffer=e.pop()||"";for(const r of e){const e=r.trim();if(e)try{const r=JSON.parse(e);this.traceMessage(r).catch(e=>{this.debug&&console.error("[MCP Logger CLI] Trace error:",e)})}catch(r){this.debug&&console.error("[MCP Logger CLI] Failed to parse:",e)}}}async traceMessage(e){this.debug&&console.error("[MCP Logger CLI] Tracer not initialized")}_flush(e){try{if(this.buffer.trim())try{const e=JSON.parse(this.buffer);this.traceMessage(e).catch(e=>{this.debug&&console.error("[MCP Logger CLI] Trace error:",e)})}catch(e){this.debug&&console.error("[MCP Logger CLI] Failed to parse final buffer:",e)}e()}catch(r){e(r)}}}export{r as MessageInterceptor};
1
+ import{Transform as e}from"stream";class s extends e{buffer="";direction;pairer;debug;constructor(e,s,r=!1){super(),this.direction=e,this.pairer=s,this.debug=r}_transform(e,s,r){try{this.push(e),this.buffer+=e.toString(),this.buffer.length>1e6&&(this.debug&&console.error("[mcp-logger CLI] Buffer size exceeded, clearing..."),this.buffer=""),this.tryParseMessages(),r()}catch(e){r(e)}}tryParseMessages(){const e=this.buffer.split("\n");this.buffer=e.pop()||"";for(const s of e){const e=s.trim();if(e)try{const s=JSON.parse(e);this.handleMessage(s)}catch(s){this.debug&&console.error("[mcp-logger CLI] Failed to parse:",e.slice(0,100))}}}handleMessage(e){"request"===this.direction&&e.method?this.pairer.onRequest(e):"response"!==this.direction||void 0===e.id||e.method||this.pairer.onResponse(e)}_flush(e){try{if(this.buffer.trim())try{const e=JSON.parse(this.buffer);this.handleMessage(e)}catch(e){this.debug&&console.error("[mcp-logger CLI] Failed to parse final buffer")}if(this.debug){const e=this.pairer.getStats();console.error("[mcp-logger CLI] Stream closed. Pending requests: "+e.pending)}e()}catch(s){e(s)}}}export{s as MessageInterceptor};
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ function e(){return h}function r(){return I}async function t(){if(d)try{await d.shutdown(),d=null,h=null,f=!1,I=null}catch(e){console.error("[MCP Logger] Error shutting down telemetry:",e)}}function s(e,r=1e4){try{const t=JSON.stringify(e);return t.length>r?t.slice(0,r)+"... (truncated)":t}catch(e){return null}}function o(t){const o=e();if(!o)return void console.error("[MCP Logger] Tracer not initialized, cannot create span");const n="mcp."+t.method;o.startActiveSpan(n,e=>{try{const o={"mcp.method":t.method,"mcp.source":t.source,"mcp.transport":t.transport,"mcp.log_type":"request-response","mcp.request.id":t.request.id+"","mcp.request.timestamp":t.request.timestamp,"mcp.request.params":s(t.request.params)||"{}","mcp.request.params.size":JSON.stringify(t.request.params||{}).length,"mcp.response.timestamp":t.response.timestamp,"mcp.duration_ms":t.duration};if("tools/call"===t.method&&t.request.params){const e=t.request.params.name,r=t.request.params.arguments;if(e&&(o["mcp.tool.name"]=e),r){const e=s(r);e&&(o["mcp.tool.arguments"]=e,o["mcp.tool.arguments.size"]=JSON.stringify(r).length)}}if(void 0!==t.response.result){const e=s(t.response.result);e&&(o["mcp.response.result"]=e,o["mcp.response.result.size"]=JSON.stringify(t.response.result).length)}if(t.request.headers){const e=s(t.request.headers);e&&(o["mcp.headers"]=e,o["mcp.headers.size"]=JSON.stringify(t.request.headers).length)}if("cli"===t.source){const e=r();e&&(o["mcp.cli.server"]=JSON.stringify({command:e.command,args:e.args,env:e.env}))}if(t.customEvents&&t.customEvents.length>0&&(o["mcp.events"]=JSON.stringify(t.customEvents),o["mcp.events.count"]=t.customEvents.length,t.customEvents.forEach(r=>{e.addEvent("custom."+(r.level||"info"),{message:r.message,metadata:JSON.stringify(r.metadata||{}),timestamp:r.timestamp||Date.now()})})),t.response.error){o["mcp.error"]=!0;const r=t.response.error instanceof Error?t.response.error.message:t.response.error+"";o["mcp.error.message"]=r,t.response.error instanceof Error&&e.recordException(t.response.error),e.setStatus({code:u.ERROR,message:r})}else e.setStatus({code:u.OK});!function(e,r){for(const[t,s]of Object.entries(r))null!=s&&e.setAttribute(t,s)}(e,o)}catch(r){console.error("[MCP Logger] Error creating paired span:",r),e.setStatus({code:u.ERROR,message:r+""})}finally{e.end()}})}import{spawn as n}from"child_process";import{program as i}from"commander";import{NodeSDK as c}from"@opentelemetry/sdk-node";import{OTLPTraceExporter as a}from"@opentelemetry/exporter-trace-otlp-http";import{Resource as p}from"@opentelemetry/resources";import{trace as m,SpanStatusCode as u}from"@opentelemetry/api";import{Transform as l}from"stream";const g={ENDPOINT:"https://aware.mcypher.com/v1/traces",SDK_VERSION:"1.0.0",SERVICE_NAME_PREFIX:"mcp-server",TRACKED_METHODS:["tools/call","tools/list"]};let d=null,h=null,f=!1,I=null;class y extends l{buffer="";direction;pairer;debug;constructor(e,r,t=!1){super(),this.direction=e,this.pairer=r,this.debug=t}_transform(e,r,t){try{this.push(e),this.buffer+=e.toString(),this.buffer.length>1e6&&(this.debug&&console.error("[mcp-logger CLI] Buffer size exceeded, clearing..."),this.buffer=""),this.tryParseMessages(),t()}catch(e){t(e)}}tryParseMessages(){const e=this.buffer.split("\n");this.buffer=e.pop()||"";for(const r of e){const e=r.trim();if(e)try{const r=JSON.parse(e);this.handleMessage(r)}catch(r){this.debug&&console.error("[mcp-logger CLI] Failed to parse:",e.slice(0,100))}}}handleMessage(e){"request"===this.direction&&e.method?this.pairer.onRequest(e):"response"!==this.direction||void 0===e.id||e.method||this.pairer.onResponse(e)}_flush(e){try{if(this.buffer.trim())try{const e=JSON.parse(this.buffer);this.handleMessage(e)}catch(e){this.debug&&console.error("[mcp-logger CLI] Failed to parse final buffer")}if(this.debug){const e=this.pairer.getStats();console.error("[mcp-logger CLI] Stream closed. Pending requests: "+e.pending)}e()}catch(r){e(r)}}}class S{pendingRequests=new Map;debug;TIMEOUT=3e4;constructor(e=!1){this.debug=e}onRequest(e){var r;r=e.method,g.TRACKED_METHODS.includes(r)?e.id?(this.pendingRequests.set(e.id,{request:e,timestamp:Date.now()}),this.debug&&console.error(`[mcp-logger CLI] Request stored: ${e.method} (ID: ${e.id})`),setTimeout(()=>this.cleanupStale(e.id),this.TIMEOUT)):this.debug&&console.error("[mcp-logger CLI] Request without ID, skipping:",e.method):this.debug&&console.error("[mcp-logger CLI] Skipping method: "+e.method)}onResponse(e){if(!e.id)return void(this.debug&&console.error("[mcp-logger CLI] Response without ID, skipping"));const r=this.pendingRequests.get(e.id);if(!r)return void(this.debug&&console.error("[mcp-logger CLI] No matching request for response ID: "+e.id));const t=Date.now(),s=t-r.timestamp;this.debug&&console.error(`[mcp-logger CLI] Pairing success: ${r.request.method} (${s}ms)`),this.createPairedSpan(r.request,e,r.timestamp,t,s),this.pendingRequests.delete(e.id)}createPairedSpan(e,r,t,s,n){o({method:e.method,source:"cli",transport:"stdio",request:{id:e.id,timestamp:t,params:e.params},response:{timestamp:s,result:r.result,error:r.error},duration:n})}cleanupStale(e){const r=this.pendingRequests.get(e);r&&Date.now()-r.timestamp>this.TIMEOUT&&(this.debug&&console.error(`[mcp-logger CLI] Request timeout: ${r.request.method} (ID: ${e})`),this.pendingRequests.delete(e))}getStats(){return{pending:this.pendingRequests.size}}clear(){this.debug&&console.error(`[mcp-logger CLI] Clearing ${this.pendingRequests.size} pending requests`),this.pendingRequests.clear()}}i.name("mcp-logger").description("Add observability to any MCP server without code changes").version("1.0.0").requiredOption("-k, --api-key <key>","Aware API key").option("-s, --service-name <name>","Service name for identification").option("-e, --endpoint <url>","Custom OTLP endpoint").argument("<command...>","MCP server command to wrap").action(async(e,r)=>{try{const s=!0;!function(e){if(f)return console.error("[MCP Logger] Telemetry already initialized, returning existing tracer"),h;const r=e.serviceName||`${g.SERVICE_NAME_PREFIX}-${Math.random().toString(36).slice(2,8)}`;console.error("[MCP Logger] Initializing telemetry..."),console.error("[MCP Logger] Endpoint: "+(e.endpoint||g.ENDPOINT)),console.error("[MCP Logger] Service: "+r),d=new c({resource:new p({"service.name":r,"service.version":g.SDK_VERSION}),traceExporter:new a({url:e.endpoint||g.ENDPOINT,headers:{"x-api-key":e.apiKey}})}),d.start(),h=m.getTracer("mcp-logger",g.SDK_VERSION),f=!0,console.error("[MCP Logger] Telemetry initialized successfully");const s=async()=>{console.error("[MCP Logger] Shutting down telemetry..."),await t()};process.once("SIGTERM",s),process.once("SIGINT",s)}({...r,debug:s}),console.error("[MCP Logger CLI] Starting MCP server:",e.join(" "));const[o,...i]=e;!function(e,r,t){I={command:e,args:r,env:t}}(o,i,{...process.env});const u=n(o,i,{stdio:["pipe","pipe","inherit"],env:{...process.env,MCP_LOGGER_ENABLED:"true"}}),l=new S(s),C=new y("request",l,s);process.stdin.pipe(C).pipe(u.stdin);const E=new y("response",l,s);u.stdout.pipe(E).pipe(process.stdout),u.on("error",async e=>{console.error("[MCP Logger CLI] Failed to start MCP server:",e),l.clear(),await t(),process.exit(1)}),u.on("exit",async(e,r)=>{console.error(`[MCP Logger CLI] MCP server exited with code ${e}, signal ${r}`),l.clear(),await t(),process.exit(e||0)});let q=!1;const v=async e=>{q||(q=!0,console.error(`[MCP Logger CLI] Received ${e}, shutting down...`),u.kill(e),await Promise.race([new Promise(e=>u.once("exit",e)),new Promise(e=>setTimeout(e,5e3))]),await t(),process.exit(0))};process.on("SIGTERM",()=>v("SIGTERM")),process.on("SIGINT",()=>v("SIGINT"))}catch(e){console.error("[MCP Logger CLI] Fatal error:",e),await t(),process.exit(1)}}),i.parse();
@@ -0,0 +1 @@
1
+ import"@opentelemetry/api";import"@opentelemetry/sdk-node";import"@opentelemetry/exporter-trace-otlp-http";import"@opentelemetry/resources";const e={TRACKED_METHODS:["tools/call","tools/list"]};class t{pendingRequests=new Map;debug;TIMEOUT=3e4;constructor(e=!1){this.debug=e}onRequest(t){var s;s=t.method,e.TRACKED_METHODS.includes(s)?t.id?(this.pendingRequests.set(t.id,{request:t,timestamp:Date.now()}),this.debug&&console.error(`[mcp-logger CLI] Request stored: ${t.method} (ID: ${t.id})`),setTimeout(()=>this.cleanupStale(t.id),this.TIMEOUT)):this.debug&&console.error("[mcp-logger CLI] Request without ID, skipping:",t.method):this.debug&&console.error("[mcp-logger CLI] Skipping method: "+t.method)}onResponse(e){if(!e.id)return void(this.debug&&console.error("[mcp-logger CLI] Response without ID, skipping"));const t=this.pendingRequests.get(e.id);if(!t)return void(this.debug&&console.error("[mcp-logger CLI] No matching request for response ID: "+e.id));const s=Date.now(),o=s-t.timestamp;this.debug&&console.error(`[mcp-logger CLI] Pairing success: ${t.request.method} (${o}ms)`),this.createPairedSpan(t.request,e,t.timestamp,s,o),this.pendingRequests.delete(e.id)}createPairedSpan(e,t,s,o,r){e.method,e.id,e.params,t.result,t.error,console.error("[MCP Logger] Tracer not initialized, cannot create span")}cleanupStale(e){const t=this.pendingRequests.get(e);t&&Date.now()-t.timestamp>this.TIMEOUT&&(this.debug&&console.error(`[mcp-logger CLI] Request timeout: ${t.request.method} (ID: ${e})`),this.pendingRequests.delete(e))}getStats(){return{pending:this.pendingRequests.size}}clear(){this.debug&&console.error(`[mcp-logger CLI] Clearing ${this.pendingRequests.size} pending requests`),this.pendingRequests.clear()}}export{t as RequestResponsePairer};
@@ -0,0 +1 @@
1
+ function t(t){return E.TRACKED_METHODS.includes(t)}const E={ENDPOINT:"https://aware.mcypher.com/v1/traces",SDK_VERSION:"1.0.0",SERVICE_NAME_PREFIX:"mcp-server",TRACKED_METHODS:["tools/call","tools/list"]};export{E as CONFIG,t as shouldTrackMethod};
@@ -0,0 +1 @@
1
+ function e(e){console.error("[MCP Logger] Tracer not initialized, cannot create span")}import"@opentelemetry/api";import"@opentelemetry/sdk-node";import"@opentelemetry/exporter-trace-otlp-http";import"@opentelemetry/resources";export{e as createPairedSpan};
@@ -0,0 +1 @@
1
+ function t(t,r){for(const[e,n]of Object.entries(r))null!=n&&t.setAttribute(e,n)}function r(t,r,e,n){try{e?t.addEvent(r,e):t.addEvent(r)}catch(t){n&&console.error("[MCP Logger] Failed to add span event:",t)}}function e(t,r,e){const n=r instanceof Error?r.message:"Unknown error",o=r instanceof Error?r.constructor.name:"Error";r instanceof Error&&t.recordException(r),t.setStatus({code:s.ERROR,message:n}),t.setAttribute("error",!0),t.setAttribute("error.type",o),t.setAttribute("error.message",n),e&&console.error("[MCP Logger] Error recorded in span:",r)}function n(t){t.setStatus({code:s.OK})}function o(t,r=1e4){try{const e=JSON.stringify(t);return e.length>r?e.slice(0,r)+"... (truncated)":e}catch(t){return null}}function c(){const t={},r=a.active(),e=i.getSpan(r);if(e){const r=e.spanContext();r&&(t.traceparent=`00-${r.traceId}-${r.spanId}-01`)}return t}import{SpanStatusCode as s,context as a,trace as i}from"@opentelemetry/api";export{r as addSpanEvent,c as getTraceHeaders,n as markSpanSuccess,e as recordSpanError,o as safeStringify,t as setSpanAttributes};
@@ -0,0 +1 @@
1
+ function e(e){if(y)return e.debug&&console.error("[MCP Logger] Telemetry already initialized, returning existing tracer"),d;const r=e.serviceName||`${m}-${Math.random().toString(36).slice(2,8)}`;e.debug&&(console.error("[MCP Logger] Initializing telemetry..."),console.error("[MCP Logger] Endpoint: "+(e.endpoint||u)),console.error("[MCP Logger] Service: "+r)),p=new c({resource:new s({"service.name":r,"service.version":g}),traceExporter:new l({url:e.endpoint||u,headers:{"x-api-key":e.apiKey}})}),p.start(),d=a.getTracer("mcp-logger",g),y=!0,e.debug&&console.error("[MCP Logger] Telemetry initialized successfully");const o=async()=>{e.debug&&console.error("[MCP Logger] Shutting down telemetry..."),await i()};return process.once("SIGTERM",o),process.once("SIGINT",o),d}function r(){return d}function o(){return y}function t(e,r,o){f={command:e,args:r,env:o}}function n(){return f}async function i(){if(p)try{await p.shutdown(),p=null,d=null,y=!1,f=null}catch(e){console.error("[MCP Logger] Error shutting down telemetry:",e)}}import{NodeSDK as c}from"@opentelemetry/sdk-node";import{OTLPTraceExporter as l}from"@opentelemetry/exporter-trace-otlp-http";import{Resource as s}from"@opentelemetry/resources";import{trace as a}from"@opentelemetry/api";const u="https://aware.mcypher.com/v1/traces",g="1.0.0",m="mcp-server";let p=null,d=null,y=!1,f=null;export{n as getCLIServerInfo,r as getTracer,e as initializeTelemetry,o as isInitializedTelemetry,t as setCLIServerInfo,i as shutdownTelemetry};
@@ -0,0 +1,174 @@
1
+ /**
2
+ * 공통 설정 상수
3
+ */
4
+ declare const CONFIG: {
5
+ /**
6
+ * OTLP Endpoint (하드코딩)
7
+ */
8
+ readonly ENDPOINT: "https://aware.mcypher.com/v1/traces";
9
+ /**
10
+ * SDK 버전
11
+ */
12
+ readonly SDK_VERSION: "1.0.0";
13
+ /**
14
+ * 서비스 이름 접두사
15
+ */
16
+ readonly SERVICE_NAME_PREFIX: "mcp-server";
17
+ /**
18
+ * 추적 대상 메서드
19
+ * CLI와 SDK 모두에서 공통으로 사용
20
+ */
21
+ readonly TRACKED_METHODS: readonly ["tools/call", "tools/list"];
22
+ };
23
+ /**
24
+ * 추적 대상 메서드 타입
25
+ */
26
+ type TrackedMethod = typeof CONFIG.TRACKED_METHODS[number];
27
+
28
+ /**
29
+ * 공통 타입 정의
30
+ */
31
+
32
+ /**
33
+ * 텔레메트리 초기화 옵션
34
+ */
35
+ interface TelemetryOptions {
36
+ /**
37
+ * API Key for authentication (필수)
38
+ */
39
+ apiKey: string;
40
+ /**
41
+ * 서비스 이름 (옵션)
42
+ * 지정하지 않으면 자동으로 'mcp-server-{random}' 형식으로 생성
43
+ */
44
+ serviceName?: string;
45
+ /**
46
+ * 커스텀 OTLP 엔드포인트 (옵션)
47
+ * 기본값: CONFIG.ENDPOINT
48
+ */
49
+ endpoint?: string;
50
+ /**
51
+ * 디버그 로그 활성화 (옵션)
52
+ */
53
+ debug?: boolean;
54
+ }
55
+ /**
56
+ * Span 속성 인터페이스
57
+ */
58
+ interface SpanAttributes {
59
+ [key: string]: string | number | boolean | undefined;
60
+ }
61
+ /**
62
+ * MCP 메시지 방향
63
+ */
64
+ type MessageDirection = 'request' | 'response';
65
+ /**
66
+ * 로그 타입 분류
67
+ */
68
+ type MCPLogType = 'request-response' | 'custom-event' | 'internal-event';
69
+ /**
70
+ * JSON-RPC 메시지 타입
71
+ */
72
+ interface JSONRPCMessage {
73
+ jsonrpc: '2.0';
74
+ id?: string | number;
75
+ method?: string;
76
+ params?: any;
77
+ result?: any;
78
+ error?: {
79
+ code: number;
80
+ message: string;
81
+ data?: any;
82
+ };
83
+ }
84
+ /**
85
+ * JSON-RPC Request 타입
86
+ */
87
+ interface JSONRPCRequest extends JSONRPCMessage {
88
+ method: string;
89
+ params?: any;
90
+ }
91
+ /**
92
+ * JSON-RPC Response 타입
93
+ */
94
+ interface JSONRPCResponse extends JSONRPCMessage {
95
+ id: string | number;
96
+ result?: any;
97
+ error?: {
98
+ code: number;
99
+ message: string;
100
+ data?: any;
101
+ };
102
+ }
103
+ /**
104
+ * MCP Span 데이터 구조 (ES 최적화)
105
+ */
106
+ interface MCPSpanData {
107
+ 'mcp.method': TrackedMethod;
108
+ 'mcp.source': 'cli' | 'sdk';
109
+ 'mcp.transport': 'stdio' | 'sse';
110
+ 'mcp.log_type': MCPLogType;
111
+ 'mcp.request.id': string;
112
+ 'mcp.request.timestamp': number;
113
+ 'mcp.request.params': string;
114
+ 'mcp.request.params.size': number;
115
+ 'mcp.response.timestamp': number;
116
+ 'mcp.response.result'?: string;
117
+ 'mcp.response.result.size'?: number;
118
+ 'mcp.duration_ms': number;
119
+ 'mcp.tool.name'?: string;
120
+ 'mcp.tool.arguments'?: string;
121
+ 'mcp.tool.arguments.size'?: number;
122
+ 'mcp.headers'?: string;
123
+ 'mcp.headers.size'?: number;
124
+ 'mcp.events'?: string;
125
+ 'mcp.events.count'?: number;
126
+ 'mcp.cli.server'?: string;
127
+ 'mcp.error'?: boolean;
128
+ 'mcp.error.code'?: number;
129
+ 'mcp.error.message'?: string;
130
+ }
131
+ /**
132
+ * 커스텀 로그 엔트리
133
+ */
134
+ interface CustomLogEntry {
135
+ /**
136
+ * 로그 레벨
137
+ */
138
+ level?: 'info' | 'warn' | 'error';
139
+ /**
140
+ * 로그 메시지
141
+ */
142
+ message: string;
143
+ /**
144
+ * 추가 메타데이터 (자유 형식)
145
+ */
146
+ metadata?: Record<string, any>;
147
+ /**
148
+ * 타임스탬프 (자동 생성 가능)
149
+ */
150
+ timestamp?: number;
151
+ }
152
+ /**
153
+ * Paired Span 입력 데이터
154
+ */
155
+ interface PairedSpanInput {
156
+ method: TrackedMethod;
157
+ source: 'cli' | 'sdk';
158
+ transport: 'stdio' | 'sse';
159
+ request: {
160
+ id: string | number;
161
+ timestamp: number;
162
+ params: any;
163
+ headers?: Record<string, string>;
164
+ };
165
+ response: {
166
+ timestamp: number;
167
+ result?: any;
168
+ error?: any;
169
+ };
170
+ duration: number;
171
+ customEvents?: CustomLogEntry[];
172
+ }
173
+
174
+ export type { CustomLogEntry, JSONRPCMessage, JSONRPCRequest, JSONRPCResponse, MCPLogType, MCPSpanData, MessageDirection, PairedSpanInput, SpanAttributes, TelemetryOptions };
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
3
  /**
4
4
  * 공통 타입 정의
5
5
  */
6
+
6
7
  /**
7
8
  * 텔레메트리 초기화 옵션
8
9
  */
@@ -26,32 +27,45 @@ interface TelemetryOptions {
26
27
  */
27
28
  debug?: boolean;
28
29
  }
30
+ /**
31
+ * 커스텀 로그 엔트리
32
+ */
33
+ interface CustomLogEntry$1 {
34
+ /**
35
+ * 로그 레벨
36
+ */
37
+ level?: 'info' | 'warn' | 'error';
38
+ /**
39
+ * 로그 메시지
40
+ */
41
+ message: string;
42
+ /**
43
+ * 추가 메타데이터 (자유 형식)
44
+ */
45
+ metadata?: Record<string, any>;
46
+ /**
47
+ * 타임스탬프 (자동 생성 가능)
48
+ */
49
+ timestamp?: number;
50
+ }
29
51
 
30
52
  /**
31
53
  * SDK 타입 정의
32
54
  */
33
55
 
34
56
  /**
35
- * SDK 초기화 옵션 (TelemetryOptions 확장)
57
+ * Trace 옵션
36
58
  */
37
- interface SDKInitOptions extends TelemetryOptions {
59
+ interface TraceOptions extends TelemetryOptions {
38
60
  }
39
61
  /**
40
- * MCP Server 타입 (export)
41
- */
42
- type MCPServer = Server;
43
-
44
- /**
45
- * 공통 텔레메트리 레이어
46
- *
47
- * OpenTelemetry SDK 초기화 및 관리를 담당합니다.
48
- * SDK와 CLI 양쪽에서 공통으로 사용됩니다.
62
+ * Custom Log Entry (re-export from core)
49
63
  */
50
-
64
+ type CustomLogEntry = CustomLogEntry$1;
51
65
  /**
52
- * Telemetry 종료
66
+ * MCP Server 타입 (재export)
53
67
  */
54
- declare function shutdownTelemetry(): Promise<void>;
68
+ type MCPServer = Server;
55
69
 
56
70
  /**
57
71
  * MCP Logger SDK
@@ -60,56 +74,64 @@ declare function shutdownTelemetry(): Promise<void>;
60
74
  */
61
75
 
62
76
  /**
63
- * MCP 서버에 자동 계측을 추가합니다.
64
- *
65
- * 이 함수는 MCP 서버의 모든 요청/응답을 자동으로 로깅하고 추적합니다.
66
- * OpenTelemetry를 사용하여 traces를 수집하며, 설정된 엔드포인트로 전송합니다.
67
- *
68
- * @param server - MCP Server 인스턴스
69
- * @param options - 초기화 옵션
70
- *
71
- * @throws {Error} apiKey가 제공되지 않은 경우
72
- * @throws {Error} server 인스턴스가 제공되지 않은 경우
77
+ * trace 네임스페이스
73
78
  *
74
79
  * @example
75
- * TypeScript 사용 예시:
76
80
  * ```typescript
77
- * import { Server } from '@modelcontextprotocol/sdk/server/index.js';
78
- * import { initMCPLogger } from '@awarecorp/mcp-logger';
79
- *
80
- * const server = new Server({ name: 'my-server', version: '1.0.0' }, {
81
- * capabilities: { tools: {} }
82
- * });
81
+ * import { trace } from '@awarecorp/mcp-logger';
83
82
  *
84
- * initMCPLogger(server, {
83
+ * // 서버 계측
84
+ * trace(server, {
85
85
  * apiKey: 'your-api-key',
86
- * serviceName: 'my-mcp-server',
87
- * debug: true,
86
+ * serviceName: 'my-service',
88
87
  * });
89
88
  *
89
+ * // Handler 내부에서 커스텀 로그 추가
90
90
  * server.setRequestHandler('tools/call', async (request) => {
91
- * // 이 핸들러는 자동으로 로깅됩니다
92
- * return { result: 'success' };
93
- * });
94
- * ```
91
+ * trace.addLog({
92
+ * level: 'info',
93
+ * message: 'Processing started',
94
+ * metadata: { id: request.params.arguments.id },
95
+ * });
95
96
  *
96
- * @example
97
- * JavaScript 사용 예시:
98
- * ```javascript
99
- * const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
100
- * const { initMCPLogger } = require('@awarecorp/mcp-logger');
97
+ * const result = await process();
101
98
  *
102
- * const server = new Server({ name: 'my-server', version: '1.0.0' }, {
103
- * capabilities: { tools: {} }
104
- * });
99
+ * trace.addLog({
100
+ * level: 'info',
101
+ * message: 'Processing completed',
102
+ * metadata: { duration: 100 },
103
+ * });
105
104
  *
106
- * initMCPLogger(server, {
107
- * apiKey: process.env.API_KEY,
108
- * serviceName: 'my-mcp-server',
105
+ * return result;
109
106
  * });
110
107
  * ```
111
108
  */
112
- declare function initMCPLogger(server: MCPServer, options: SDKInitOptions): void;
109
+ declare const trace: ((server: MCPServer, options: TraceOptions) => void) & {
110
+ /**
111
+ * 현재 active span에 커스텀 로그 추가
112
+ *
113
+ * ⚠️ 주의: Request handler 내부에서만 호출해야 합니다.
114
+ *
115
+ * @param entry - 커스텀 로그 엔트리
116
+ *
117
+ * @example
118
+ * ```typescript
119
+ * trace.addLog({
120
+ * level: 'info',
121
+ * message: 'Database query executed',
122
+ * metadata: { query: 'SELECT * FROM users', rows: 42 },
123
+ * });
124
+ * ```
125
+ */
126
+ addLog(entry: CustomLogEntry$1): void;
127
+ /**
128
+ * Telemetry 종료
129
+ *
130
+ * 일반적으로 명시적으로 호출할 필요는 없습니다.
131
+ * 프로세스 종료 시 자동으로 처리됩니다.
132
+ */
133
+ shutdown(): Promise<void>;
134
+ };
113
135
 
114
- export { initMCPLogger, shutdownTelemetry as shutdown };
115
- export type { MCPServer, SDKInitOptions };
136
+ export { trace };
137
+ export type { CustomLogEntry, MCPServer, TraceOptions };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{NodeSDK as e}from"@opentelemetry/sdk-node";import{OTLPTraceExporter as r}from"@opentelemetry/exporter-trace-otlp-http";import{Resource as t}from"@opentelemetry/resources";import{trace as o,SpanStatusCode as n}from"@opentelemetry/api";const s="https://aware.mcypher.com/v1/traces",c="1.0.0";let i=null,a=null,l=!1;function u(){return a}async function g(){if(i)try{await i.shutdown(),i=null,a=null,l=!1}catch(e){console.error("[MCP Logger] Error shutting down telemetry:",e)}}function m(e,r){for(const[t,o]of Object.entries(r))null!=o&&e.setAttribute(t,o)}function p(e,r,t,o){try{t?e.addEvent(r,t):e.addEvent(r)}catch(e){o&&console.error("[MCP Logger] Failed to add span event:",e)}}function d(e,r=1e4){try{const t=JSON.stringify(e);return t.length>r?t.slice(0,r)+"... (truncated)":t}catch(e){return null}}function h(h,f){if(!f.apiKey)throw new Error("[MCP Logger SDK] apiKey is required");if(!h)throw new Error("[MCP Logger SDK] server instance is required");!function(n){if(l)return n.debug&&console.error("[MCP Logger] Telemetry already initialized, returning existing tracer"),a;const u=n.serviceName||`mcp-server-${Math.random().toString(36).slice(2,8)}`;n.debug&&(console.error("[MCP Logger] Initializing telemetry..."),console.error(`[MCP Logger] Endpoint: ${n.endpoint||s}`),console.error(`[MCP Logger] Service: ${u}`)),i=new e({resource:new t({"service.name":u,"service.version":c}),traceExporter:new r({url:n.endpoint||s,headers:{"x-api-key":n.apiKey}})}),i.start(),a=o.getTracer("mcp-logger",c),l=!0,n.debug&&console.error("[MCP Logger] Telemetry initialized successfully");const m=async()=>{n.debug&&console.error("[MCP Logger] Shutting down telemetry..."),await g()};process.once("SIGTERM",m),process.once("SIGINT",m)}(f),function(e,r=!1){const t=u();if(!t)throw new Error("[MCP Logger SDK] Tracer not initialized. Call initMCPLogger first.");const o=e.setRequestHandler.bind(e);e.setRequestHandler=function(e,s){const c=e.shape?.method,i=c?._def?.value||"unknown";return r&&console.log(`[MCP Logger SDK] Instrumenting handler: ${i}`),o(e,async(e,o)=>{const c=`mcp.${i}`;return await t.startActiveSpan(c,{attributes:{"mcp.method":i,"mcp.request.method":e.method}},async t=>{const c=Date.now();try{const i=d(e.params||{});m(t,{"mcp.request.id":"unknown","mcp.request.params.size":i?.length}),i&&i.length<1e3&&t.setAttribute("mcp.request.params",i),p(t,"mcp.request.received",{"request.method":e.method,"request.timestamp":c},r),r&&console.log("[MCP Logger SDK] Request received:",{method:e.method,params:e.params});const a=await s(e,o),l=Date.now(),u=l-c,g=d(a);return m(t,{"mcp.duration_ms":u,"mcp.response.size":g?.length}),g&&g.length<1e3&&t.setAttribute("mcp.response.result",g),p(t,"mcp.response.sent",{"response.timestamp":l,"response.duration_ms":u},r),function(e){e.setStatus({code:n.OK})}(t),r&&console.log(`[MCP Logger SDK] Response sent (${u}ms):`,a),a}catch(e){throw function(e,r,t){const o=r instanceof Error?r.message:"Unknown error",s=r instanceof Error?r.constructor.name:"Error";r instanceof Error&&e.recordException(r),e.setStatus({code:n.ERROR,message:o}),e.setAttribute("error",!0),e.setAttribute("error.type",s),e.setAttribute("error.message",o),t&&console.error("[MCP Logger] Error recorded in span:",r)}(t,e,r),r&&console.error("[MCP Logger SDK] Error occurred:",e),e}finally{t.end()}})})},r&&console.log("[MCP Logger SDK] Server instrumentation complete")}(h,f.debug),f.debug&&console.log("[MCP Logger SDK] ✓ Initialization complete")}export{h as initMCPLogger,g as shutdown};
1
+ function e(){return g}function t(){return f}async function r(){if(u)try{await u.shutdown(),u=null,g=null,d=!1,f=null}catch(e){console.error("[MCP Logger] Error shutting down telemetry:",e)}}function s(e,t=1e4){try{const r=JSON.stringify(e);return r.length>t?r.slice(0,t)+"... (truncated)":r}catch(e){return null}}function o(r){const o=e();if(!o)return void console.error("[MCP Logger] Tracer not initialized, cannot create span");const n="mcp."+r.method;o.startActiveSpan(n,e=>{try{const o={"mcp.method":r.method,"mcp.source":r.source,"mcp.transport":r.transport,"mcp.log_type":"request-response","mcp.request.id":r.request.id+"","mcp.request.timestamp":r.request.timestamp,"mcp.request.params":s(r.request.params)||"{}","mcp.request.params.size":JSON.stringify(r.request.params||{}).length,"mcp.response.timestamp":r.response.timestamp,"mcp.duration_ms":r.duration};if("tools/call"===r.method&&r.request.params){const e=r.request.params.name,t=r.request.params.arguments;if(e&&(o["mcp.tool.name"]=e),t){const e=s(t);e&&(o["mcp.tool.arguments"]=e,o["mcp.tool.arguments.size"]=JSON.stringify(t).length)}}if(void 0!==r.response.result){const e=s(r.response.result);e&&(o["mcp.response.result"]=e,o["mcp.response.result.size"]=JSON.stringify(r.response.result).length)}if(r.request.headers){const e=s(r.request.headers);e&&(o["mcp.headers"]=e,o["mcp.headers.size"]=JSON.stringify(r.request.headers).length)}if("cli"===r.source){const e=t();e&&(o["mcp.cli.server"]=JSON.stringify({command:e.command,args:e.args,env:e.env}))}if(r.customEvents&&r.customEvents.length>0&&(o["mcp.events"]=JSON.stringify(r.customEvents),o["mcp.events.count"]=r.customEvents.length,r.customEvents.forEach(t=>{e.addEvent("custom."+(t.level||"info"),{message:t.message,metadata:JSON.stringify(t.metadata||{}),timestamp:t.timestamp||Date.now()})})),r.response.error){o["mcp.error"]=!0;const t=r.response.error instanceof Error?r.response.error.message:r.response.error+"";o["mcp.error.message"]=t,r.response.error instanceof Error&&e.recordException(r.response.error),e.setStatus({code:a.ERROR,message:t})}else e.setStatus({code:a.OK});!function(e,t){for(const[r,s]of Object.entries(t))null!=s&&e.setAttribute(r,s)}(e,o)}catch(t){console.error("[MCP Logger] Error creating paired span:",t),e.setStatus({code:a.ERROR,message:t+""})}finally{e.end()}})}import{trace as n,SpanStatusCode as a,context as c}from"@opentelemetry/api";import{NodeSDK as i}from"@opentelemetry/sdk-node";import{OTLPTraceExporter as m}from"@opentelemetry/exporter-trace-otlp-http";import{Resource as l}from"@opentelemetry/resources";const p={ENDPOINT:"https://aware.mcypher.com/v1/traces",SDK_VERSION:"1.0.0",SERVICE_NAME_PREFIX:"mcp-server",TRACKED_METHODS:["tools/call","tools/list"]};let u=null,g=null,d=!1,f=null;const h=Symbol("mcp-logger.customEvents"),E=Object.assign(function(e,t){if(!t.apiKey)throw Error("[mcp-logger] apiKey is required");if(!e)throw Error("[mcp-logger] server instance is required");!function(e){if(d)return e.debug&&console.error("[MCP Logger] Telemetry already initialized, returning existing tracer"),g;const t=e.serviceName||`${p.SERVICE_NAME_PREFIX}-${Math.random().toString(36).slice(2,8)}`;e.debug&&(console.error("[MCP Logger] Initializing telemetry..."),console.error("[MCP Logger] Endpoint: "+(e.endpoint||p.ENDPOINT)),console.error("[MCP Logger] Service: "+t)),u=new i({resource:new l({"service.name":t,"service.version":p.SDK_VERSION}),traceExporter:new m({url:e.endpoint||p.ENDPOINT,headers:{"x-api-key":e.apiKey}})}),u.start(),g=n.getTracer("mcp-logger",p.SDK_VERSION),d=!0,e.debug&&console.error("[MCP Logger] Telemetry initialized successfully");const s=async()=>{e.debug&&console.error("[MCP Logger] Shutting down telemetry..."),await r()};process.once("SIGTERM",s),process.once("SIGINT",s)}(t),function(e,t){const r=e.setRequestHandler.bind(e);e.setRequestHandler=function(e,s){const n=e.shape?.method,a=n?._def?.value||"unknown";return function(e){return p.TRACKED_METHODS.includes(e)}(a)?(t.debug&&console.log("[mcp-logger] Instrumenting handler: "+a),r(e,async(e,r)=>{const n=Date.now(),i=`${a}-${n}-${Math.random().toString(36).slice(2,8)}`,m=[],l=c.active().setValue(h,m);try{const p=await c.with(l,async()=>await s(e,r)),u=Date.now();return o({method:a,source:"sdk",transport:"stdio",request:{id:i,timestamp:n,params:e.params},response:{timestamp:u,result:p},duration:u-n,customEvents:m.length>0?m:void 0}),t.debug&&console.log(`[mcp-logger] Request completed (${u-n}ms):`,{method:a,customEvents:m.length}),p}catch(r){const s=Date.now();throw o({method:a,source:"sdk",transport:"stdio",request:{id:i,timestamp:n,params:e.params},response:{timestamp:s,error:r},duration:s-n,customEvents:m.length>0?m:void 0}),t.debug&&console.error("[mcp-logger] Request failed:",r),r}})):(t.debug&&console.log("[mcp-logger] Skipping instrumentation for method: "+a),r(e,s))},t.debug&&console.log("[mcp-logger] Server instrumentation complete")}(e,t),t.debug&&console.log("[mcp-logger] ✓ Initialization complete")},{addLog(e){const t=c.active().getValue(h);if(!t)return void console.warn("[mcp-logger] addLog called outside of handler context");const r={level:e.level||"info",message:e.message,metadata:e.metadata,timestamp:e.timestamp||Date.now()};t.push(r);const s=n.getSpan(c.active());s&&s.addEvent("custom."+r.level,{message:r.message,metadata:JSON.stringify(r.metadata||{}),timestamp:r.timestamp})},async shutdown(){await r()}});export{E as trace};
package/dist/sdk/index.js CHANGED
@@ -1 +1 @@
1
- import{NodeSDK as e}from"@opentelemetry/sdk-node";import{OTLPTraceExporter as r}from"@opentelemetry/exporter-trace-otlp-http";import{Resource as t}from"@opentelemetry/resources";import{trace as o,SpanStatusCode as n}from"@opentelemetry/api";const s="https://aware.mcypher.com/v1/traces",c="1.0.0";let i=null,a=null,l=!1;function u(){return a}async function g(){if(i)try{await i.shutdown(),i=null,a=null,l=!1}catch(e){console.error("[MCP Logger] Error shutting down telemetry:",e)}}function m(e,r){for(const[t,o]of Object.entries(r))null!=o&&e.setAttribute(t,o)}function p(e,r,t,o){try{t?e.addEvent(r,t):e.addEvent(r)}catch(e){o&&console.error("[MCP Logger] Failed to add span event:",e)}}function d(e,r=1e4){try{const t=JSON.stringify(e);return t.length>r?t.slice(0,r)+"... (truncated)":t}catch(e){return null}}function h(h,f){if(!f.apiKey)throw new Error("[MCP Logger SDK] apiKey is required");if(!h)throw new Error("[MCP Logger SDK] server instance is required");!function(n){if(l)return n.debug&&console.error("[MCP Logger] Telemetry already initialized, returning existing tracer"),a;const u=n.serviceName||`mcp-server-${Math.random().toString(36).slice(2,8)}`;n.debug&&(console.error("[MCP Logger] Initializing telemetry..."),console.error(`[MCP Logger] Endpoint: ${n.endpoint||s}`),console.error(`[MCP Logger] Service: ${u}`)),i=new e({resource:new t({"service.name":u,"service.version":c}),traceExporter:new r({url:n.endpoint||s,headers:{"x-api-key":n.apiKey}})}),i.start(),a=o.getTracer("mcp-logger",c),l=!0,n.debug&&console.error("[MCP Logger] Telemetry initialized successfully");const m=async()=>{n.debug&&console.error("[MCP Logger] Shutting down telemetry..."),await g()};process.once("SIGTERM",m),process.once("SIGINT",m)}(f),function(e,r=!1){const t=u();if(!t)throw new Error("[MCP Logger SDK] Tracer not initialized. Call initMCPLogger first.");const o=e.setRequestHandler.bind(e);e.setRequestHandler=function(e,s){const c=e.shape?.method,i=c?._def?.value||"unknown";return r&&console.log(`[MCP Logger SDK] Instrumenting handler: ${i}`),o(e,async(e,o)=>{const c=`mcp.${i}`;return await t.startActiveSpan(c,{attributes:{"mcp.method":i,"mcp.request.method":e.method}},async t=>{const c=Date.now();try{const i=d(e.params||{});m(t,{"mcp.request.id":"unknown","mcp.request.params.size":i?.length}),i&&i.length<1e3&&t.setAttribute("mcp.request.params",i),p(t,"mcp.request.received",{"request.method":e.method,"request.timestamp":c},r),r&&console.log("[MCP Logger SDK] Request received:",{method:e.method,params:e.params});const a=await s(e,o),l=Date.now(),u=l-c,g=d(a);return m(t,{"mcp.duration_ms":u,"mcp.response.size":g?.length}),g&&g.length<1e3&&t.setAttribute("mcp.response.result",g),p(t,"mcp.response.sent",{"response.timestamp":l,"response.duration_ms":u},r),function(e){e.setStatus({code:n.OK})}(t),r&&console.log(`[MCP Logger SDK] Response sent (${u}ms):`,a),a}catch(e){throw function(e,r,t){const o=r instanceof Error?r.message:"Unknown error",s=r instanceof Error?r.constructor.name:"Error";r instanceof Error&&e.recordException(r),e.setStatus({code:n.ERROR,message:o}),e.setAttribute("error",!0),e.setAttribute("error.type",s),e.setAttribute("error.message",o),t&&console.error("[MCP Logger] Error recorded in span:",r)}(t,e,r),r&&console.error("[MCP Logger SDK] Error occurred:",e),e}finally{t.end()}})})},r&&console.log("[MCP Logger SDK] Server instrumentation complete")}(h,f.debug),f.debug&&console.log("[MCP Logger SDK] ✓ Initialization complete")}export{h as initMCPLogger,g as shutdown};
1
+ function e(){return g}function t(){return f}async function r(){if(u)try{await u.shutdown(),u=null,g=null,d=!1,f=null}catch(e){console.error("[MCP Logger] Error shutting down telemetry:",e)}}function s(e,t=1e4){try{const r=JSON.stringify(e);return r.length>t?r.slice(0,t)+"... (truncated)":r}catch(e){return null}}function o(r){const o=e();if(!o)return void console.error("[MCP Logger] Tracer not initialized, cannot create span");const n="mcp."+r.method;o.startActiveSpan(n,e=>{try{const o={"mcp.method":r.method,"mcp.source":r.source,"mcp.transport":r.transport,"mcp.log_type":"request-response","mcp.request.id":r.request.id+"","mcp.request.timestamp":r.request.timestamp,"mcp.request.params":s(r.request.params)||"{}","mcp.request.params.size":JSON.stringify(r.request.params||{}).length,"mcp.response.timestamp":r.response.timestamp,"mcp.duration_ms":r.duration};if("tools/call"===r.method&&r.request.params){const e=r.request.params.name,t=r.request.params.arguments;if(e&&(o["mcp.tool.name"]=e),t){const e=s(t);e&&(o["mcp.tool.arguments"]=e,o["mcp.tool.arguments.size"]=JSON.stringify(t).length)}}if(void 0!==r.response.result){const e=s(r.response.result);e&&(o["mcp.response.result"]=e,o["mcp.response.result.size"]=JSON.stringify(r.response.result).length)}if(r.request.headers){const e=s(r.request.headers);e&&(o["mcp.headers"]=e,o["mcp.headers.size"]=JSON.stringify(r.request.headers).length)}if("cli"===r.source){const e=t();e&&(o["mcp.cli.server"]=JSON.stringify({command:e.command,args:e.args,env:e.env}))}if(r.customEvents&&r.customEvents.length>0&&(o["mcp.events"]=JSON.stringify(r.customEvents),o["mcp.events.count"]=r.customEvents.length,r.customEvents.forEach(t=>{e.addEvent("custom."+(t.level||"info"),{message:t.message,metadata:JSON.stringify(t.metadata||{}),timestamp:t.timestamp||Date.now()})})),r.response.error){o["mcp.error"]=!0;const t=r.response.error instanceof Error?r.response.error.message:r.response.error+"";o["mcp.error.message"]=t,r.response.error instanceof Error&&e.recordException(r.response.error),e.setStatus({code:a.ERROR,message:t})}else e.setStatus({code:a.OK});!function(e,t){for(const[r,s]of Object.entries(t))null!=s&&e.setAttribute(r,s)}(e,o)}catch(t){console.error("[MCP Logger] Error creating paired span:",t),e.setStatus({code:a.ERROR,message:t+""})}finally{e.end()}})}import{trace as n,SpanStatusCode as a,context as c}from"@opentelemetry/api";import{NodeSDK as i}from"@opentelemetry/sdk-node";import{OTLPTraceExporter as m}from"@opentelemetry/exporter-trace-otlp-http";import{Resource as l}from"@opentelemetry/resources";const p={ENDPOINT:"https://aware.mcypher.com/v1/traces",SDK_VERSION:"1.0.0",SERVICE_NAME_PREFIX:"mcp-server",TRACKED_METHODS:["tools/call","tools/list"]};let u=null,g=null,d=!1,f=null;const h=Symbol("mcp-logger.customEvents"),E=Object.assign(function(e,t){if(!t.apiKey)throw Error("[mcp-logger] apiKey is required");if(!e)throw Error("[mcp-logger] server instance is required");!function(e){if(d)return e.debug&&console.error("[MCP Logger] Telemetry already initialized, returning existing tracer"),g;const t=e.serviceName||`${p.SERVICE_NAME_PREFIX}-${Math.random().toString(36).slice(2,8)}`;e.debug&&(console.error("[MCP Logger] Initializing telemetry..."),console.error("[MCP Logger] Endpoint: "+(e.endpoint||p.ENDPOINT)),console.error("[MCP Logger] Service: "+t)),u=new i({resource:new l({"service.name":t,"service.version":p.SDK_VERSION}),traceExporter:new m({url:e.endpoint||p.ENDPOINT,headers:{"x-api-key":e.apiKey}})}),u.start(),g=n.getTracer("mcp-logger",p.SDK_VERSION),d=!0,e.debug&&console.error("[MCP Logger] Telemetry initialized successfully");const s=async()=>{e.debug&&console.error("[MCP Logger] Shutting down telemetry..."),await r()};process.once("SIGTERM",s),process.once("SIGINT",s)}(t),function(e,t){const r=e.setRequestHandler.bind(e);e.setRequestHandler=function(e,s){const n=e.shape?.method,a=n?._def?.value||"unknown";return function(e){return p.TRACKED_METHODS.includes(e)}(a)?(t.debug&&console.log("[mcp-logger] Instrumenting handler: "+a),r(e,async(e,r)=>{const n=Date.now(),i=`${a}-${n}-${Math.random().toString(36).slice(2,8)}`,m=[],l=c.active().setValue(h,m);try{const p=await c.with(l,async()=>await s(e,r)),u=Date.now();return o({method:a,source:"sdk",transport:"stdio",request:{id:i,timestamp:n,params:e.params},response:{timestamp:u,result:p},duration:u-n,customEvents:m.length>0?m:void 0}),t.debug&&console.log(`[mcp-logger] Request completed (${u-n}ms):`,{method:a,customEvents:m.length}),p}catch(r){const s=Date.now();throw o({method:a,source:"sdk",transport:"stdio",request:{id:i,timestamp:n,params:e.params},response:{timestamp:s,error:r},duration:s-n,customEvents:m.length>0?m:void 0}),t.debug&&console.error("[mcp-logger] Request failed:",r),r}})):(t.debug&&console.log("[mcp-logger] Skipping instrumentation for method: "+a),r(e,s))},t.debug&&console.log("[mcp-logger] Server instrumentation complete")}(e,t),t.debug&&console.log("[mcp-logger] ✓ Initialization complete")},{addLog(e){const t=c.active().getValue(h);if(!t)return void console.warn("[mcp-logger] addLog called outside of handler context");const r={level:e.level||"info",message:e.message,metadata:e.metadata,timestamp:e.timestamp||Date.now()};t.push(r);const s=n.getSpan(c.active());s&&s.addEvent("custom."+r.level,{message:r.message,metadata:JSON.stringify(r.metadata||{}),timestamp:r.timestamp})},async shutdown(){await r()}});export{E as trace};
@@ -1 +1 @@
1
- import"@opentelemetry/sdk-node";import"@opentelemetry/exporter-trace-otlp-http";import"@opentelemetry/resources";import"@opentelemetry/api";function e(e,t=!1){throw new Error("[MCP Logger SDK] Tracer not initialized. Call initMCPLogger first.")}export{e as instrumentMCPServer};
1
+ function e(e){console.error("[MCP Logger] Tracer not initialized, cannot create span")}function t(t,o){const l=t.setRequestHandler.bind(t);t.setRequestHandler=function(t,s){const a=t.shape?.method,i=a?._def?.value||"unknown";return function(e){return r.TRACKED_METHODS.includes(e)}(i)?(o.debug&&console.log("[mcp-logger] Instrumenting handler: "+i),l(t,async(t,r)=>{const l=Date.now(),a=(Math.random().toString(36).slice(2,8),[]),m=n.active().setValue(c,a);try{const c=await n.with(m,async()=>await s(t,r)),u=Date.now();return e(t.params),o.debug&&console.log(`[mcp-logger] Request completed (${u-l}ms):`,{method:i,customEvents:a.length}),c}catch(n){throw e(t.params),o.debug&&console.error("[mcp-logger] Request failed:",n),n}})):(o.debug&&console.log("[mcp-logger] Skipping instrumentation for method: "+i),l(t,s))},o.debug&&console.log("[mcp-logger] Server instrumentation complete")}function o(){return n.active().getValue(c)}import{context as n}from"@opentelemetry/api";import"@opentelemetry/sdk-node";import"@opentelemetry/exporter-trace-otlp-http";import"@opentelemetry/resources";const r={TRACKED_METHODS:["tools/call","tools/list"]},c=Symbol("mcp-logger.customEvents");export{o as getCustomEventsFromContext,t as instrumentMCPServer};
@@ -5,6 +5,7 @@ import { Request, Result } from '@modelcontextprotocol/sdk/types.js';
5
5
  /**
6
6
  * 공통 타입 정의
7
7
  */
8
+
8
9
  /**
9
10
  * 텔레메트리 초기화 옵션
10
11
  */
@@ -28,16 +29,41 @@ interface TelemetryOptions {
28
29
  */
29
30
  debug?: boolean;
30
31
  }
32
+ /**
33
+ * 커스텀 로그 엔트리
34
+ */
35
+ interface CustomLogEntry$1 {
36
+ /**
37
+ * 로그 레벨
38
+ */
39
+ level?: 'info' | 'warn' | 'error';
40
+ /**
41
+ * 로그 메시지
42
+ */
43
+ message: string;
44
+ /**
45
+ * 추가 메타데이터 (자유 형식)
46
+ */
47
+ metadata?: Record<string, any>;
48
+ /**
49
+ * 타임스탬프 (자동 생성 가능)
50
+ */
51
+ timestamp?: number;
52
+ }
31
53
 
32
54
  /**
33
55
  * SDK 타입 정의
34
56
  */
35
57
 
36
58
  /**
37
- * SDK 초기화 옵션 (TelemetryOptions 확장)
59
+ * Trace 옵션
38
60
  */
39
- interface SDKInitOptions extends TelemetryOptions {
61
+ interface TraceOptions extends TelemetryOptions {
40
62
  }
63
+ /**
64
+ * Custom Log Entry (re-export from core)
65
+ */
66
+ type CustomLogEntry = CustomLogEntry$1;
41
67
  /**
42
68
  * MCP JSON-RPC Request 타입
43
69
  */
@@ -63,4 +89,4 @@ interface RequestHandlerExtra {
63
89
  */
64
90
  type MCPServer = Server;
65
91
 
66
- export type { MCPRequest, MCPRequestSchema, MCPResult, MCPServer, RequestHandlerExtra, SDKInitOptions };
92
+ export type { CustomLogEntry, MCPRequest, MCPRequestSchema, MCPResult, MCPServer, RequestHandlerExtra, TraceOptions };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awarecorp/mcp-logger",
3
- "version": "0.0.2-dev.3",
3
+ "version": "0.0.2",
4
4
  "description": "Unified MCP observability solution - SDK for code integration and CLI for zero-config wrapping",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -19,6 +19,9 @@
19
19
  "prepublishOnly": "npm run build",
20
20
  "clean": "rm -rf dist",
21
21
  "dev": "tsc --watch",
22
+ "test": "mocha",
23
+ "test:watch": "mocha --watch",
24
+ "test:coverage": "c8 mocha",
22
25
  "test:cli": "npm run build:readable && node bin/mcp-logger.js --help",
23
26
  "publish:stable": "npm run build && npm publish --access public",
24
27
  "publish:dev": "npm run build && npm publish --access public --tag dev",
@@ -40,6 +43,14 @@
40
43
  ],
41
44
  "author": "Aware",
42
45
  "license": "MIT",
46
+ "homepage": "https://github.com/awarecorp/mcp-logger#readme",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "https://github.com/awarecorp/mcp-logger.git"
50
+ },
51
+ "bugs": {
52
+ "url": "https://github.com/awarecorp/mcp-logger/issues"
53
+ },
43
54
  "peerDependencies": {
44
55
  "@modelcontextprotocol/sdk": "^0.5.0"
45
56
  },
@@ -60,10 +71,15 @@
60
71
  "@modelcontextprotocol/sdk": "^0.5.0",
61
72
  "@rollup/plugin-terser": "^0.4.4",
62
73
  "@rollup/plugin-typescript": "^12.1.4",
74
+ "@types/chai": "^5.2.3",
75
+ "@types/mocha": "^10.0.10",
63
76
  "@types/node": "^20.0.0",
77
+ "chai": "^6.2.0",
78
+ "mocha": "^11.7.4",
64
79
  "rollup": "^4.52.5",
65
80
  "rollup-plugin-dts": "^6.2.3",
66
81
  "tslib": "^2.8.1",
82
+ "tsx": "^4.20.6",
67
83
  "typescript": "^5.3.0"
68
84
  },
69
85
  "engines": {
package/dist/cli/cli.js DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- import{spawn as e}from"child_process";import{program as r}from"commander";import{NodeSDK as t}from"@opentelemetry/sdk-node";import{OTLPTraceExporter as o}from"@opentelemetry/exporter-trace-otlp-http";import{Resource as s}from"@opentelemetry/resources";import{trace as i,SpanStatusCode as n}from"@opentelemetry/api";import{Transform as c}from"stream";const a="https://aware.mcypher.com/v1/traces",p="1.0.0";let g=null,d=null,m=!1;function l(){return d}async function u(){if(g)try{await g.shutdown(),g=null,d=null,m=!1}catch(e){console.error("[MCP Logger] Error shutting down telemetry:",e)}}function h(e,r=1e4){try{const t=JSON.stringify(e);return t.length>r?t.slice(0,r)+"... (truncated)":t}catch(e){return null}}class f extends c{buffer="";direction;debug;constructor(e,r=!1){super(),this.direction=e,this.debug=r}_transform(e,r,t){try{this.push(e),this.buffer+=e.toString(),this.buffer.length>1e6&&(this.debug&&console.error("[MCP Logger CLI] Buffer size exceeded, clearing..."),this.buffer=""),this.tryParseMessages(),t()}catch(e){t(e)}}tryParseMessages(){const e=this.buffer.split("\n");this.buffer=e.pop()||"";for(const r of e){const e=r.trim();if(e)try{const r=JSON.parse(e);this.traceMessage(r).catch(e=>{this.debug&&console.error("[MCP Logger CLI] Trace error:",e)})}catch(r){this.debug&&console.error("[MCP Logger CLI] Failed to parse:",e)}}}async traceMessage(e){const r=l();if(!r)return void(this.debug&&console.error("[MCP Logger CLI] Tracer not initialized"));const t=e.method?`mcp.${e.method}`:void 0!==e.id?"mcp.response":"mcp.notification";return new Promise(o=>{r.startActiveSpan(t,r=>{try{if(function(e,r){for(const[t,o]of Object.entries(r))null!=o&&e.setAttribute(t,o)}(r,{"mcp.direction":this.direction,"mcp.jsonrpc":e.jsonrpc||"2.0"}),void 0!==e.id&&r.setAttribute("mcp.id",String(e.id)),e.method&&r.setAttribute("mcp.method",e.method),e.params){const t=h(e.params);t&&(r.setAttribute("mcp.params.size",t.length),t.length<1e3&&r.setAttribute("mcp.params",t))}const t=h(e);t&&function(e,r,t,o){try{t?e.addEvent(r,t):e.addEvent(r)}catch(e){o&&console.error("[MCP Logger] Failed to add span event:",e)}}(r,`mcp.${this.direction}`,{message:t,timestamp:Date.now()},this.debug),this.debug&&console.error(`[MCP Logger CLI] ${this.direction}:`,JSON.stringify(e,null,2)),e.error?(r.setAttribute("mcp.error",!0),r.setAttribute("mcp.error.code",e.error.code),r.setAttribute("mcp.error.message",e.error.message),r.setStatus({code:n.ERROR,message:e.error.message})):function(e){e.setStatus({code:n.OK})}(r)}catch(e){this.debug&&console.error("[MCP Logger CLI] Error in span:",e),r.setStatus({code:n.ERROR,message:String(e)})}finally{r.end(),o()}})})}_flush(e){try{if(this.buffer.trim())try{const e=JSON.parse(this.buffer);this.traceMessage(e).catch(e=>{this.debug&&console.error("[MCP Logger CLI] Trace error:",e)})}catch(e){this.debug&&console.error("[MCP Logger CLI] Failed to parse final buffer:",e)}e()}catch(r){e(r)}}}r.name("mcp-logger").description("Add observability to any MCP server without code changes").version("1.0.0").requiredOption("-k, --api-key <key>","Aware API key").option("-s, --service-name <name>","Service name for identification").option("-e, --endpoint <url>","Custom OTLP endpoint").argument("<command...>","MCP server command to wrap").action(async(r,n)=>{try{const c=!0;!function(e){if(m)return console.error("[MCP Logger] Telemetry already initialized, returning existing tracer"),d;const r=e.serviceName||`mcp-server-${Math.random().toString(36).slice(2,8)}`;console.error("[MCP Logger] Initializing telemetry..."),console.error(`[MCP Logger] Endpoint: ${e.endpoint||a}`),console.error(`[MCP Logger] Service: ${r}`),g=new t({resource:new s({"service.name":r,"service.version":p}),traceExporter:new o({url:e.endpoint||a,headers:{"x-api-key":e.apiKey}})}),g.start(),d=i.getTracer("mcp-logger",p),m=!0,console.error("[MCP Logger] Telemetry initialized successfully");const n=async()=>{console.error("[MCP Logger] Shutting down telemetry..."),await u()};process.once("SIGTERM",n),process.once("SIGINT",n)}({...n,debug:c}),console.error("[MCP Logger CLI] Starting MCP server:",r.join(" "));const[l,...h]=r,C=e(l,h,{stdio:["pipe","pipe","inherit"],env:{...process.env,MCP_LOGGER_ENABLED:"true"}}),y=new f("request",c);process.stdin.pipe(y).pipe(C.stdin);const L=new f("response",c);C.stdout.pipe(L).pipe(process.stdout),C.on("error",async e=>{console.error("[MCP Logger CLI] Failed to start MCP server:",e),await u(),process.exit(1)}),C.on("exit",async(e,r)=>{console.error(`[MCP Logger CLI] MCP server exited with code ${e}, signal ${r}`),await u(),process.exit(e||0)});let M=!1;const P=async e=>{M||(M=!0,console.error(`[MCP Logger CLI] Received ${e}, shutting down...`),C.kill(e),await Promise.race([new Promise(e=>C.once("exit",e)),new Promise(e=>setTimeout(e,5e3))]),await u(),process.exit(0))};process.on("SIGTERM",()=>P("SIGTERM")),process.on("SIGINT",()=>P("SIGINT"))}catch(e){console.error("[MCP Logger CLI] Fatal error:",e),await u(),process.exit(1)}}),r.parse();
@@ -1 +0,0 @@
1
- const e={ENDPOINT:"https://aware.mcypher.com/v1/traces",SDK_VERSION:"1.0.0",SERVICE_NAME_PREFIX:"mcp-server"};export{e as CONFIG};
@@ -1 +0,0 @@
1
- import{SpanStatusCode as t,context as r,trace as e}from"@opentelemetry/api";function n(t,r){for(const[e,n]of Object.entries(r))null!=n&&t.setAttribute(e,n)}function o(t,r,e,n){try{e?t.addEvent(r,e):t.addEvent(r)}catch(t){n&&console.error("[MCP Logger] Failed to add span event:",t)}}function c(r,e,n){const o=e instanceof Error?e.message:"Unknown error",c=e instanceof Error?e.constructor.name:"Error";e instanceof Error&&r.recordException(e),r.setStatus({code:t.ERROR,message:o}),r.setAttribute("error",!0),r.setAttribute("error.type",c),r.setAttribute("error.message",o),n&&console.error("[MCP Logger] Error recorded in span:",e)}function s(r){r.setStatus({code:t.OK})}function a(t,r=1e4){try{const e=JSON.stringify(t);return e.length>r?e.slice(0,r)+"... (truncated)":e}catch(t){return null}}function i(){const t={},n=r.active(),o=e.getSpan(n);if(o){const r=o.spanContext();r&&(t.traceparent=`00-${r.traceId}-${r.spanId}-01`)}return t}export{o as addSpanEvent,i as getTraceHeaders,s as markSpanSuccess,c as recordSpanError,a as safeStringify,n as setSpanAttributes};
@@ -1 +0,0 @@
1
- import{NodeSDK as e}from"@opentelemetry/sdk-node";import{OTLPTraceExporter as r}from"@opentelemetry/exporter-trace-otlp-http";import{Resource as o}from"@opentelemetry/resources";import{trace as t}from"@opentelemetry/api";const n="https://aware.mcypher.com/v1/traces",i="1.0.0";let c=null,s=null,l=!1;function a(a){if(l)return a.debug&&console.error("[MCP Logger] Telemetry already initialized, returning existing tracer"),s;const g=a.serviceName||`mcp-server-${Math.random().toString(36).slice(2,8)}`;a.debug&&(console.error("[MCP Logger] Initializing telemetry..."),console.error(`[MCP Logger] Endpoint: ${a.endpoint||n}`),console.error(`[MCP Logger] Service: ${g}`)),c=new e({resource:new o({"service.name":g,"service.version":i}),traceExporter:new r({url:a.endpoint||n,headers:{"x-api-key":a.apiKey}})}),c.start(),s=t.getTracer("mcp-logger",i),l=!0,a.debug&&console.error("[MCP Logger] Telemetry initialized successfully");const p=async()=>{a.debug&&console.error("[MCP Logger] Shutting down telemetry..."),await u()};return process.once("SIGTERM",p),process.once("SIGINT",p),s}function g(){return s}function p(){return l}async function u(){if(c)try{await c.shutdown(),c=null,s=null,l=!1}catch(e){console.error("[MCP Logger] Error shutting down telemetry:",e)}}export{g as getTracer,a as initializeTelemetry,p as isInitializedTelemetry,u as shutdownTelemetry};
@@ -1,53 +0,0 @@
1
- /**
2
- * 공통 타입 정의
3
- */
4
- /**
5
- * 텔레메트리 초기화 옵션
6
- */
7
- interface TelemetryOptions {
8
- /**
9
- * API Key for authentication (필수)
10
- */
11
- apiKey: string;
12
- /**
13
- * 서비스 이름 (옵션)
14
- * 지정하지 않으면 자동으로 'mcp-server-{random}' 형식으로 생성
15
- */
16
- serviceName?: string;
17
- /**
18
- * 커스텀 OTLP 엔드포인트 (옵션)
19
- * 기본값: CONFIG.ENDPOINT
20
- */
21
- endpoint?: string;
22
- /**
23
- * 디버그 로그 활성화 (옵션)
24
- */
25
- debug?: boolean;
26
- }
27
- /**
28
- * Span 속성 인터페이스
29
- */
30
- interface SpanAttributes {
31
- [key: string]: string | number | boolean | undefined;
32
- }
33
- /**
34
- * MCP 메시지 방향
35
- */
36
- type MessageDirection = 'request' | 'response';
37
- /**
38
- * JSON-RPC 메시지 타입
39
- */
40
- interface JSONRPCMessage {
41
- jsonrpc: '2.0';
42
- id?: string | number;
43
- method?: string;
44
- params?: any;
45
- result?: any;
46
- error?: {
47
- code: number;
48
- message: string;
49
- data?: any;
50
- };
51
- }
52
-
53
- export type { JSONRPCMessage, MessageDirection, SpanAttributes, TelemetryOptions };