@kyrontha/lens-mcp 0.1.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.
Potentially problematic release.
This version of @kyrontha/lens-mcp might be problematic. Click here for more details.
- package/README.md +94 -0
- package/package.json +42 -0
- package/src/index.js +213 -0
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Kyrontha Lens MCP server
|
|
2
|
+
|
|
3
|
+
[Model Context Protocol](https://modelcontextprotocol.io) server for [Kyrontha Lens](https://lens.kyrontha.io). Lets Claude Desktop, Claude Code, Cursor, Windsurf, and other MCP clients query your Lens workspace — logs, traces, APM — without leaving the editor.
|
|
4
|
+
|
|
5
|
+
## Tools exposed
|
|
6
|
+
|
|
7
|
+
| Tool | What it does |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `search_logs` | Lucene query across your log stream. Returns matching lines with timestamp, level, source, log group, and a truncated message. |
|
|
10
|
+
| `search_traces` | Search distributed traces. Filter by service, operation, status, min duration; sort by recent, slow, or errors. |
|
|
11
|
+
| `get_trace` | Fetch every span in one trace (up to 100). Use after `search_traces` to drill in. |
|
|
12
|
+
| `check_connection` | Health probe — verifies the API key works and shows which Lens workspace you're authenticated against. |
|
|
13
|
+
|
|
14
|
+
All tools are read-only. Retention, plan limits, and PII redaction are enforced server-side; this package adds no business logic — it's a transport shim.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g @kyrontha/lens-mcp
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or use `npx` directly in your MCP client config — no install needed.
|
|
23
|
+
|
|
24
|
+
## Configure
|
|
25
|
+
|
|
26
|
+
You need two things:
|
|
27
|
+
|
|
28
|
+
1. **Lens base URL.** Defaults to `https://lens.kyrontha.io`. If you're on a private deployment, override with `LENS_URL`.
|
|
29
|
+
2. **An API key.** Generate one in your Lens workspace under a connection's API keys, or in the admin panel. The format is `kl_…`.
|
|
30
|
+
|
|
31
|
+
### Claude Desktop
|
|
32
|
+
|
|
33
|
+
Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"mcpServers": {
|
|
38
|
+
"kyrontha-lens": {
|
|
39
|
+
"command": "npx",
|
|
40
|
+
"args": ["-y", "@kyrontha/lens-mcp"],
|
|
41
|
+
"env": {
|
|
42
|
+
"LENS_URL": "https://lens.kyrontha.io",
|
|
43
|
+
"LENS_API_KEY": "kl_…"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Restart Claude Desktop. Ask: *"Use kyrontha-lens to find any errors in the last hour."*
|
|
51
|
+
|
|
52
|
+
### Claude Code
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
claude mcp add kyrontha-lens npx @kyrontha/lens-mcp \
|
|
56
|
+
-e LENS_URL=https://lens.kyrontha.io \
|
|
57
|
+
-e LENS_API_KEY=kl_…
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Cursor, Windsurf, etc.
|
|
61
|
+
|
|
62
|
+
These read the same `mcpServers` config shape. Drop the JSON block above into the client's MCP config.
|
|
63
|
+
|
|
64
|
+
## Try it
|
|
65
|
+
|
|
66
|
+
After configuring, a few useful prompts:
|
|
67
|
+
|
|
68
|
+
> *"In Lens, what were the top error sources in the last 24h?"*
|
|
69
|
+
|
|
70
|
+
> *"Find any traces where checkout took over 2 seconds today. Pull the slowest one's full span tree."*
|
|
71
|
+
|
|
72
|
+
> *"Grep my logs for 'connection refused' over the last 3 days, then check if there are matching traces."*
|
|
73
|
+
|
|
74
|
+
The model decides which tools to call. With Lens's unified store backing it, a single conversation can pull from logs + traces in one investigation.
|
|
75
|
+
|
|
76
|
+
## Troubleshooting
|
|
77
|
+
|
|
78
|
+
**`LENS_API_KEY env var is required`** — the env block in your MCP config wasn't picked up. Restart the client and confirm the key is set under the `env` key, not at the top level.
|
|
79
|
+
|
|
80
|
+
**`401 Invalid API key`** — the key is wrong or has been rotated. Regenerate in Lens and update the config.
|
|
81
|
+
|
|
82
|
+
**Tools time out** — Lens API calls have a 30s timeout. If the workspace has multi-terabyte data, narrow `time_range` first (`1h` then `6h`) before widening.
|
|
83
|
+
|
|
84
|
+
**Need verbose logs** — set `MCP_DEBUG=1` in the env block to see request/response detail on stderr.
|
|
85
|
+
|
|
86
|
+
## Security
|
|
87
|
+
|
|
88
|
+
- The API key is shipped through your MCP client's env config and never touches Lens's backend except as a Bearer header. Treat it like a password.
|
|
89
|
+
- All tools are read-only. There's currently no MCP tool that writes data into Lens.
|
|
90
|
+
- The server is a single Node.js process spawned by your MCP client; it terminates with the client.
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT.
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kyrontha/lens-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Model Context Protocol server for Kyrontha Lens — lets Claude Desktop, Claude Code, Cursor, and other MCP clients query your logs, traces, and APM data.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"kyrontha-lens-mcp": "./src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./src/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"start": "node src/index.js",
|
|
19
|
+
"test": "node --test test/*.test.js"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
23
|
+
"zod": "^3.23.0"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"mcp",
|
|
27
|
+
"model-context-protocol",
|
|
28
|
+
"observability",
|
|
29
|
+
"logs",
|
|
30
|
+
"traces",
|
|
31
|
+
"apm",
|
|
32
|
+
"kyrontha",
|
|
33
|
+
"lens"
|
|
34
|
+
],
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://dev.azure.com/jblamba/Kyrontha%20Lens/_git/Kyrontha-Lens",
|
|
38
|
+
"directory": "mcp"
|
|
39
|
+
},
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"author": "Kyrontha"
|
|
42
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Kyrontha Lens MCP server.
|
|
3
|
+
//
|
|
4
|
+
// Lets MCP clients (Claude Desktop, Claude Code, Cursor, Windsurf, etc.)
|
|
5
|
+
// query the user's Lens workspace — logs, traces, APM — directly. Every
|
|
6
|
+
// tool here is a thin shim around an HTTP endpoint on the Lens backend
|
|
7
|
+
// (/v1/query/*) authenticated by the customer's API key. The shim adds
|
|
8
|
+
// no business logic; retention clamping, redaction, plan limits all live
|
|
9
|
+
// server-side so a leaked MCP package can't bypass them.
|
|
10
|
+
//
|
|
11
|
+
// Config (read once at startup from env):
|
|
12
|
+
// LENS_URL Base URL of the Lens API. Default https://lens.kyrontha.io
|
|
13
|
+
// LENS_API_KEY Customer's API key (kl_…). Required.
|
|
14
|
+
//
|
|
15
|
+
// Distribution:
|
|
16
|
+
// npx @kyrontha/lens-mcp
|
|
17
|
+
// - or -
|
|
18
|
+
// npm install -g @kyrontha/lens-mcp
|
|
19
|
+
// kyrontha-lens-mcp
|
|
20
|
+
//
|
|
21
|
+
// Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.json
|
|
22
|
+
// on macOS, %APPDATA%\Claude\claude_desktop_config.json on Windows):
|
|
23
|
+
// {
|
|
24
|
+
// "mcpServers": {
|
|
25
|
+
// "kyrontha-lens": {
|
|
26
|
+
// "command": "npx",
|
|
27
|
+
// "args": ["-y", "@kyrontha/lens-mcp"],
|
|
28
|
+
// "env": {
|
|
29
|
+
// "LENS_URL": "https://lens.kyrontha.io",
|
|
30
|
+
// "LENS_API_KEY": "kl_…"
|
|
31
|
+
// }
|
|
32
|
+
// }
|
|
33
|
+
// }
|
|
34
|
+
// }
|
|
35
|
+
//
|
|
36
|
+
// All log output goes to stderr so it can't pollute the stdio MCP wire
|
|
37
|
+
// protocol on stdout. Claude Desktop surfaces stderr in its developer
|
|
38
|
+
// settings if you need to debug.
|
|
39
|
+
|
|
40
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
41
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
42
|
+
import { z } from 'zod';
|
|
43
|
+
|
|
44
|
+
const LENS_URL = (process.env.LENS_URL || 'https://lens.kyrontha.io').replace(/\/+$/, '');
|
|
45
|
+
const LENS_API_KEY = process.env.LENS_API_KEY || '';
|
|
46
|
+
|
|
47
|
+
if (!LENS_API_KEY) {
|
|
48
|
+
console.error(
|
|
49
|
+
'[kyrontha-lens-mcp] LENS_API_KEY env var is required.\n' +
|
|
50
|
+
' Generate one at ' + LENS_URL + '/admin or your tenant\'s connection wizard,\n' +
|
|
51
|
+
' then set it in your MCP client config (e.g. claude_desktop_config.json).');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── HTTP helpers ─────────────────────────────────────────────────────────────
|
|
56
|
+
// One thin wrapper around fetch that:
|
|
57
|
+
// - injects the bearer token
|
|
58
|
+
// - timeouts the call (slow Lens shouldn't hang the MCP client forever)
|
|
59
|
+
// - surfaces structured errors to the model as text content so it can
|
|
60
|
+
// reason about the failure (vs throwing into the MCP stack)
|
|
61
|
+
//
|
|
62
|
+
// Returned shape is the MCP tool-result shape so call sites can return it
|
|
63
|
+
// directly without an extra wrap.
|
|
64
|
+
async function lensRequest(method, path, body) {
|
|
65
|
+
const url = LENS_URL + path;
|
|
66
|
+
const controller = new AbortController();
|
|
67
|
+
const timer = setTimeout(() => controller.abort(), 30_000);
|
|
68
|
+
try {
|
|
69
|
+
const res = await fetch(url, {
|
|
70
|
+
method,
|
|
71
|
+
headers: {
|
|
72
|
+
Authorization: 'Bearer ' + LENS_API_KEY,
|
|
73
|
+
'Content-Type': 'application/json',
|
|
74
|
+
Accept: 'application/json',
|
|
75
|
+
},
|
|
76
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
77
|
+
signal: controller.signal,
|
|
78
|
+
});
|
|
79
|
+
const text = await res.text();
|
|
80
|
+
let parsed;
|
|
81
|
+
try { parsed = JSON.parse(text); } catch { parsed = { raw: text }; }
|
|
82
|
+
if (!res.ok) {
|
|
83
|
+
const errMsg =
|
|
84
|
+
(parsed && parsed.error) ||
|
|
85
|
+
`Lens ${method} ${path} returned ${res.status}`;
|
|
86
|
+
return {
|
|
87
|
+
content: [{ type: 'text', text: `Error: ${errMsg}` }],
|
|
88
|
+
isError: true,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
content: [{ type: 'text', text: JSON.stringify(parsed, null, 2) }],
|
|
93
|
+
};
|
|
94
|
+
} catch (err) {
|
|
95
|
+
const msg = err.name === 'AbortError'
|
|
96
|
+
? `Lens ${method} ${path} timed out after 30s`
|
|
97
|
+
: `Lens ${method} ${path} failed: ${err.message}`;
|
|
98
|
+
return {
|
|
99
|
+
content: [{ type: 'text', text: `Error: ${msg}` }],
|
|
100
|
+
isError: true,
|
|
101
|
+
};
|
|
102
|
+
} finally {
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Server setup ─────────────────────────────────────────────────────────────
|
|
108
|
+
const server = new McpServer({
|
|
109
|
+
name: 'kyrontha-lens',
|
|
110
|
+
version: '0.1.0',
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Tool descriptions are read by the model to decide when to call them.
|
|
114
|
+
// Be explicit about what each tool returns AND when to reach for it —
|
|
115
|
+
// generic descriptions lead to over-calling (e.g. searching logs for
|
|
116
|
+
// metrics-shaped questions) and burn tokens.
|
|
117
|
+
|
|
118
|
+
server.registerTool(
|
|
119
|
+
'search_logs',
|
|
120
|
+
{
|
|
121
|
+
title: 'Search logs',
|
|
122
|
+
description:
|
|
123
|
+
'Search the customer\'s log stream and return matching lines. Use this for questions about log events, errors, ' +
|
|
124
|
+
'request patterns, or anything text-shaped. Query syntax is Lucene: `level:error AND source:api`, ' +
|
|
125
|
+
'`message:"connection refused"`, `log_group:/aws/lambda/* AND status_code:[500 TO 599]`, ' +
|
|
126
|
+
'`trace_id:"<hex>"`. Empty query returns every log line in the time range.',
|
|
127
|
+
inputSchema: {
|
|
128
|
+
query: z.string().describe(
|
|
129
|
+
'Lucene-style query. Empty string matches all logs in the time range.'),
|
|
130
|
+
time_range: z.enum(['1h', '6h', '24h', '3d', '7d']).optional()
|
|
131
|
+
.describe('How far back to search. Default 24h.'),
|
|
132
|
+
limit: z.number().int().min(1).max(50).optional()
|
|
133
|
+
.describe('Max log lines to return (1-50). Default 20.'),
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
async (input) => lensRequest('POST', '/v1/query/logs', input),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
server.registerTool(
|
|
140
|
+
'search_traces',
|
|
141
|
+
{
|
|
142
|
+
title: 'Search distributed traces',
|
|
143
|
+
description:
|
|
144
|
+
'Search distributed traces (APM). Returns one row per trace summarising the root span. Filters match ANY ' +
|
|
145
|
+
'span in the trace (so service="email" finds traces with any email-service span). Use sort="slow" for the ' +
|
|
146
|
+
'slowest traces in the window, "errors" for traces with any error span, "recent" for most recent (default). ' +
|
|
147
|
+
'For a specific trace, call get_trace with its trace_id.',
|
|
148
|
+
inputSchema: {
|
|
149
|
+
query: z.string().optional()
|
|
150
|
+
.describe('Optional free-text filter — matches operation name + service.'),
|
|
151
|
+
service: z.string().optional()
|
|
152
|
+
.describe('Filter by service name (matches any span in the trace).'),
|
|
153
|
+
operation: z.string().optional()
|
|
154
|
+
.describe('Filter by root-span operation name.'),
|
|
155
|
+
status: z.enum(['ok', 'error']).optional()
|
|
156
|
+
.describe('Filter by trace status (any error span → error).'),
|
|
157
|
+
min_duration_ms: z.number().int().min(0).optional()
|
|
158
|
+
.describe('Only return traces longer than this many ms.'),
|
|
159
|
+
sort: z.enum(['recent', 'slow', 'errors']).optional()
|
|
160
|
+
.describe('Sort order. Default "recent".'),
|
|
161
|
+
time_range: z.enum(['1h', '6h', '24h', '3d', '7d']).optional()
|
|
162
|
+
.describe('How far back to search. Default 24h.'),
|
|
163
|
+
limit: z.number().int().min(1).max(50).optional()
|
|
164
|
+
.describe('Max traces to return (1-50). Default 20.'),
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
async (input) => lensRequest('POST', '/v1/query/traces', input),
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
server.registerTool(
|
|
171
|
+
'get_trace',
|
|
172
|
+
{
|
|
173
|
+
title: 'Get a single trace by ID',
|
|
174
|
+
description:
|
|
175
|
+
'Fetch every span in one trace, ordered. Use this after search_traces returns a trace_id you want to inspect ' +
|
|
176
|
+
'in detail. Caps at 100 spans — for huge traces, the `truncated` flag in the response tells you there\'s more. ' +
|
|
177
|
+
'Use this for "why is THIS trace slow" or "what was the dependency chain" questions.',
|
|
178
|
+
inputSchema: {
|
|
179
|
+
trace_id: z.string().regex(/^[0-9a-f]{16,32}$/i)
|
|
180
|
+
.describe('Hex-encoded trace ID (16 or 32 chars), as returned by search_traces.'),
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
async ({ trace_id }) =>
|
|
184
|
+
lensRequest('GET', '/v1/query/traces/' + encodeURIComponent(trace_id)),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
// Health is exposed as a tool (not just an internal check) because users
|
|
188
|
+
// often ask "is my Lens connection working" or "what workspace am I pointed
|
|
189
|
+
// at" — and a clean tool keeps that out of MCP-protocol-noise territory.
|
|
190
|
+
server.registerTool(
|
|
191
|
+
'check_connection',
|
|
192
|
+
{
|
|
193
|
+
title: 'Verify Lens connection + show authenticated workspace',
|
|
194
|
+
description:
|
|
195
|
+
'Confirms this MCP server can reach Lens with the configured API key and returns the authenticated tenant ' +
|
|
196
|
+
'name. Use this if other tools start failing — it isolates "auth/network broken" from "no matching data".',
|
|
197
|
+
inputSchema: {},
|
|
198
|
+
},
|
|
199
|
+
async () => lensRequest('GET', '/v1/query/health'),
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
203
|
+
async function main() {
|
|
204
|
+
const transport = new StdioServerTransport();
|
|
205
|
+
await server.connect(transport);
|
|
206
|
+
// stderr only — stdout is owned by the MCP wire protocol.
|
|
207
|
+
console.error(`[kyrontha-lens-mcp] connected. lens=${LENS_URL}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
main().catch((err) => {
|
|
211
|
+
console.error('[kyrontha-lens-mcp] fatal:', err);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
});
|