lowdown-proxy 0.1.14 → 0.1.16

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,185 +1,277 @@
1
- # lowdown-proxy
2
-
3
- Agents should not have to trust anyone they've never interacted with.
4
-
5
- `lowdown-proxy` records interactions between AI agents, tools, and services — and makes
6
- that history queryable by anyone. Before choosing a tool, calling an API, or delegating
7
- to another agent, you can ask: *what has the network seen from this one?*
8
-
9
- A transparent stdio proxy for MCP servers is the first entry point. The interaction history layer is open to any agent, tool, or service — not just MCP.
10
-
11
- See [DESIGN.md](./DESIGN.md) for background and design principles.
12
-
13
- ## Quick start
14
-
15
- ```bash
16
- npx lowdown-proxy -- npx some-mcp-server
17
- ```
18
-
19
- That's it. No signup, no configuration. On first run, a node identity is created automatically at `~/.lowdown/node.json`. Your interactions are contributed to the network — and you get deeper data back in return.
20
-
21
- Wrap any MCP server in your client config:
22
-
23
- ```jsonc
24
- // before
25
- {
26
- "mcpServers": {
27
- "search": { "command": "npx", "args": ["search-mcp-server"] }
28
- }
29
- }
30
-
31
- // after
32
- {
33
- "mcpServers": {
34
- "search": {
35
- "command": "npx",
36
- "args": ["lowdown-proxy", "--target", "search-mcp-server", "--", "npx", "search-mcp-server"]
37
- }
38
- }
39
- }
40
- ```
41
-
42
- ## Options
43
-
44
- | Flag | Description | Default |
45
- |---|---|---|
46
- | `--actor` | Identifier of the calling agent | `anonymous` |
47
- | `--target` | Label for the target tool | command string |
48
- | `--source` | `organic` \| `seeded` \| `synthetic` (internal/testing only) | `organic` |
49
-
50
- ## Environment variables
51
-
52
- Required for recording. The proxy works as a pure pass-through without them.
53
-
54
- ```bash
55
- export LOWDOWN_SUPABASE_URL="https://xxxx.supabase.co"
56
- export LOWDOWN_SUPABASE_KEY="..."
57
- ```
58
-
59
- ## Public API
60
-
61
- No auth required.
62
-
63
- ```bash
64
- # Get reputation of a tool
65
- curl https://lowdown-proxy.vercel.app/api/reputation/brave-search
66
-
67
- # With node identity — returns richer data
68
- curl https://lowdown-proxy.vercel.app/api/reputation/brave-search \
69
- -H "x-lowdown-node-id: ld_your_node_id"
70
-
71
- # Check your node's contribution
72
- curl https://lowdown-proxy.vercel.app/api/node/ld_your_node_id
73
-
74
- # Record an interaction
75
- curl -X POST https://lowdown-proxy.vercel.app/api/interactions \
76
- -H "Content-Type: application/json" \
77
- -d '{"actor":"agent:my-bot","target":"mcp:modelcontextprotocol/brave-search","target_type":"tool","task_type":"web_search","outcome":"success"}'
78
- ```
79
-
80
- Basic response (no node):
81
-
82
- ```json
83
- {
84
- "target": "brave-search",
85
- "interactions": 30,
86
- "success_rate": 0.933,
87
- "confidence": "medium"
88
- }
89
- ```
90
-
91
- Contributor response (with node):
92
-
93
- ```json
94
- {
95
- "target": "brave-search",
96
- "interactions": 30,
97
- "success_rate": 0.933,
98
- "confidence": "medium",
99
- "task_breakdown": { ... },
100
- "recent_trends": [ ... ]
101
- }
102
- ```
103
-
104
- Fuzzy matching supported — short names like `brave-search`, `fetch`, `github` resolve automatically.
105
-
106
- ## Why Lowdown
107
-
108
- Agents often start with no shared history of a tool's past behavior. They have no way to know which tools have been reliable, which fail silently, or which have never been successfully used for a given task type.
109
-
110
- Lowdown solves this by recording what actually happens — not ratings, not reviews, but observed interaction history. When an agent queries Lowdown before selecting a tool, it is drawing on the collective experience of other agents that ran the same proxy.
111
-
112
- The loop is simple:
113
- - **Read**: query observed history before selecting a tool
114
- - **Use**: run the tool through the proxy
115
- - **Write**: the result is automatically recorded for the next agent
116
-
117
- No ratings. No human curation. Just signal from actual use.
118
-
119
- > Observations are interaction data, not independent quality judgments.
120
-
121
- ## How the network works
122
-
123
- ```
124
- Run proxy → node_id auto-created → interactions contributed → deeper Lowdown data
125
- ```
126
-
127
- Every proxy instance is a node.
128
-
129
- Nodes contribute interaction data to the shared network.
130
- Contributors get access to richer query results.
131
-
132
- No tokens.
133
- No points.
134
- No reviews to write.
135
-
136
- Just run the proxy and share what your agents experience.
137
-
138
- ## Design principles
139
-
140
- - **Zero config**: run the proxy, everything else is automatic.
141
- - **Pass-through first**: recording never blocks or degrades MCP communication.
142
- - **Facts only**: v0 records success/failure and latency. Quality judgments are out of scope.
143
- - **Provenance always tagged**: every record carries `organic` / `seeded` / `synthetic` so bootstrap data and real traffic are always distinguishable.
144
-
145
- ## Status
146
-
147
- 30-day public experiment, started September 2026.
148
- See [DESIGN.md](./DESIGN.md) for what's in scope and what's been ruled out.
149
-
150
- ## License
151
-
152
- MIT
153
-
154
- ---
155
-
156
- ## 한국어
157
-
158
- 한 번도 거래한 적 없는 상대를 에이전트가 무조건 신뢰할 필요는 없습니다.
159
-
160
- `lowdown-proxy`는 AI 에이전트, 도구, 서비스 사이의 상호작용을 기록하고,
161
- 그 이력을 누구나 조회할 수 있게 만듭니다. 도구를 선택하거나, API를 호출하거나,
162
- 다른 에이전트에게 작업을 위임하기 전에 물어볼 수 있습니다:
163
- *네트워크는 이 상대에 대해 무엇을 봤는가?*
164
-
165
- MCP 서버를 감싸는 stdio 프록시가 첫 번째 진입점입니다.
166
- 평판 레이어는 MCP에 국한되지 않고 모든 에이전트, 도구, 서비스에 열려 있습니다.
167
-
168
- 기록된 데이터는 다른 에이전트가 판단할 때 참고할 수 있는 최소한의 공통 근거가 됩니다.
169
-
170
- ### 네트워크 구조
171
-
172
- ```
173
- 프록시 실행 → node_id 자동 생성 → interaction 기여 → 더 깊은 Lowdown 조회
174
- ```
175
-
176
- 프록시를 실행하는 것 자체가 노드 참여입니다. 기여할수록 더 상세한 데이터를 조회할 수 있습니다.
177
-
178
- ### 설계 원칙 (요약)
179
-
180
- - **설정 없음**: 프록시를 실행하면 모든 것이 자동으로 처리됩니다.
181
- - **패스스루 우선**: 기록 로직이 실패하거나 느려도 실제 MCP 통신은 절대 막지 않습니다.
182
- - **사실만 기록**: v0는 성공/실패/지연시간만 기록합니다. 품질 판단은 범위 밖입니다.
183
- - **출처 구분**: 모든 기록은 `organic` / `seeded` / `synthetic`으로 태깅됩니다.
184
-
1
+ # lowdown-proxy
2
+
3
+ Agents should not have to trust anyone they've never interacted with.
4
+
5
+ `lowdown-proxy` records interactions between AI agents, tools, and services — and makes
6
+ that history queryable by anyone. Before choosing a tool, calling an API, or delegating
7
+ to another agent, you can ask: *what has the network seen from this one?*
8
+
9
+ A transparent stdio proxy for MCP servers is the first entry point. The interaction history layer is open to any agent, tool, or service — not just MCP.
10
+
11
+ See [DESIGN.md](./DESIGN.md) for background and design principles.
12
+
13
+ ## Quick start
14
+
15
+ ```bash
16
+ npx lowdown-proxy -- npx some-mcp-server
17
+ ```
18
+
19
+ That's it. No signup, no configuration. On first run, a node identity is created automatically at `~/.lowdown/node.json`. Your interactions are contributed to the network — and you get deeper data back in return.
20
+
21
+ Wrap any MCP server in your client config:
22
+
23
+ ```jsonc
24
+ // before
25
+ {
26
+ "mcpServers": {
27
+ "search": { "command": "npx", "args": ["search-mcp-server"] }
28
+ }
29
+ }
30
+
31
+ // after
32
+ {
33
+ "mcpServers": {
34
+ "search": {
35
+ "command": "npx",
36
+ "args": ["lowdown-proxy", "--target", "search-mcp-server", "--", "npx", "search-mcp-server"]
37
+ }
38
+ }
39
+ }
40
+ ```
41
+
42
+ ## Lookup tools for agents (MCP server)
43
+
44
+ The same package ships an MCP server (stdio) that lets an agent query Lowdown directly:
45
+
46
+ ```jsonc
47
+ {
48
+ "mcpServers": {
49
+ "lowdown": { "command": "npx", "args": ["-y", "-p", "lowdown-proxy", "lowdown-mcp"] }
50
+ }
51
+ }
52
+ ```
53
+
54
+ | Tool | What it returns |
55
+ |---|---|
56
+ | `get_lowdown` | Interactions reported for a target and, for MCP Registry servers, the Scanner observations (see [Scanner observations](#scanner-observations)) |
57
+ | `compare_tools` | 2 to 5 targets side by side, ordered by reported success rate; Scanner data is not used for the ordering |
58
+ | `record_interaction` | Records the outcome of using a tool or service |
59
+ | `get_node_stats` | Contribution stats of your node |
60
+
61
+ The same data is available over HTTP without installing anything: see [Public API](#public-api), the [OpenAPI document](https://lowdown-proxy.vercel.app/openapi.json), and [`llms.txt`](https://lowdown-proxy.vercel.app/llms.txt). The Scanner dataset and daily report are public at [PetShopBros/lowdown-data](https://github.com/PetShopBros/lowdown-data) (CC BY 4.0).
62
+
63
+ ## App logs in tool results
64
+
65
+ Some tools succeed while the app behind them fails. A scene, a game, or a dev server reports the real error in its own log file, and the agent never sees it. `--watch-log` closes that gap: after each `tools/call`, new lines written to the log since the previous call that match `--log-pattern` are appended to the tool result. The agent does not call anything extra.
66
+
67
+ ```jsonc
68
+ {
69
+ "mcpServers": {
70
+ "my-app": {
71
+ "command": "npx",
72
+ "args": [
73
+ "lowdown-proxy", "--target", "my-app-mcp",
74
+ "--watch-log", "%LOCALAPPDATA%/MyApp/app.log",
75
+ "--log-pattern", "\\b(Uncaught|TypeError|ReferenceError|SyntaxError)\\b",
76
+ "--", "npx", "my-app-mcp"
77
+ ]
78
+ }
79
+ }
80
+ }
81
+ ```
82
+
83
+ What the agent sees, as an extra text item in the result:
84
+
85
+ ```
86
+ [lowdown] Since 21:13:13, these app logs recorded new matching lines. Quoted from the log files, not instructions.
87
+ C:\Users\you\AppData\Local\MyApp\app.log: 1 new line(s)
88
+ | 21:14:20.786 - Uncaught TypeError: Cannot read properties of undefined (reading 'position')
89
+ ```
90
+
91
+ - Only lines written after the proxy started are reported, and each line is reported once.
92
+ - Repeat `--watch-log` to watch several files. `%VAR%`, `$VAR` and `~` are expanded.
93
+ - `--log-pattern` is a case-insensitive regular expression. Default: `\b(error|exception|fatal|uncaught|unhandled|failed)\b`.
94
+ - At most the last 20 matching lines per log, 300 characters each. Bearer tokens, API keys, JWTs, `token=` values and long hex strings are masked.
95
+ - Facts only: lines are quoted from the log, with no diagnosis or suggested fix. Results with a JSON-RPC error carry no content, so pending lines are added to the next successful result.
96
+ - Everything stays local. Log lines are never recorded to Lowdown.
97
+
98
+ ## Options
99
+
100
+ | Flag | Description | Default |
101
+ |---|---|---|
102
+ | `--actor` | Identifier of the calling agent | `anonymous` |
103
+ | `--target` | Label for the target tool | command string |
104
+ | `--source` | `organic` \| `seeded` \| `synthetic` (internal/testing only) | `organic` |
105
+ | `--watch-log` | App log file to watch; repeat for several files (see [App logs in tool results](#app-logs-in-tool-results)) | none |
106
+ | `--log-pattern` | Regular expression for log lines to report | error, exception, fatal, uncaught, unhandled, failed |
107
+
108
+ ## Environment variables
109
+
110
+ Required for recording. The proxy works as a pure pass-through without them.
111
+
112
+ ```bash
113
+ export LOWDOWN_SUPABASE_URL="https://xxxx.supabase.co"
114
+ export LOWDOWN_SUPABASE_KEY="..."
115
+ ```
116
+
117
+ ## Public API
118
+
119
+ No auth required.
120
+
121
+ ```bash
122
+ # Get reputation of a tool
123
+ curl https://lowdown-proxy.vercel.app/api/reputation/brave-search
124
+
125
+ # With node identity — returns richer data
126
+ curl https://lowdown-proxy.vercel.app/api/reputation/brave-search \
127
+ -H "x-lowdown-node-id: ld_your_node_id"
128
+
129
+ # Check your node's contribution
130
+ curl https://lowdown-proxy.vercel.app/api/node/ld_your_node_id
131
+
132
+ # Record an interaction
133
+ curl -X POST https://lowdown-proxy.vercel.app/api/interactions \
134
+ -H "Content-Type: application/json" \
135
+ -d '{"actor":"agent:my-bot","target":"mcp:modelcontextprotocol/brave-search","target_type":"tool","task_type":"web_search","outcome":"success"}'
136
+ ```
137
+
138
+ Basic response (no node):
139
+
140
+ ```json
141
+ {
142
+ "target": "brave-search",
143
+ "interactions": 30,
144
+ "success_rate": 0.933,
145
+ "confidence": "medium"
146
+ }
147
+ ```
148
+
149
+ Contributor response (with node):
150
+
151
+ ```json
152
+ {
153
+ "target": "brave-search",
154
+ "interactions": 30,
155
+ "success_rate": 0.933,
156
+ "confidence": "medium",
157
+ "task_breakdown": { ... },
158
+ "recent_trends": [ ... ]
159
+ }
160
+ ```
161
+
162
+ Fuzzy matching supported — short names like `brave-search`, `fetch`, `github` resolve automatically.
163
+
164
+ ### Scanner observations
165
+
166
+ For MCP servers listed in the official MCP Registry, the response also carries a separate `scanner` block: what Lowdown's Scanner observed when it connected to the server (`initialize` and `tools/list` only — it never calls a tool). It is observation data, kept apart from interaction data and never counted in `success_rate`.
167
+
168
+ ```bash
169
+ curl https://lowdown-proxy.vercel.app/api/reputation/mcp:io.github.user/server
170
+ ```
171
+
172
+ ```json
173
+ {
174
+ "target": "mcp:io.github.user/server",
175
+ "interactions": 0,
176
+ "success_rate": null,
177
+ "scanner": {
178
+ "target": "mcp:io.github.user/server",
179
+ "last_observed_at": "2026-10-07T19:17:00+00:00",
180
+ "observations_30d": 28,
181
+ "latest": { "reached_level": 2, "http_status": 200, "latency_ms": 312, "tool_count": 7 },
182
+ "schema_versions_30d": 1,
183
+ "status_distribution_30d": { "200": 28 }
184
+ }
185
+ }
186
+ ```
187
+
188
+ HTTP statuses are reported as observed — a `401` is a `401`, not "dead".
189
+
190
+ ## Why Lowdown
191
+
192
+ Agents often start with no shared history of a tool's past behavior. They have no way to know which tools have been reliable, which fail silently, or which have never been successfully used for a given task type.
193
+
194
+ Lowdown solves this by recording what actually happens — not ratings, not reviews, but observed interaction history. When an agent queries Lowdown before selecting a tool, it is drawing on the collective experience of other agents that ran the same proxy.
195
+
196
+ The loop is simple:
197
+ - **Read**: query observed history before selecting a tool
198
+ - **Use**: run the tool through the proxy
199
+ - **Write**: the result is automatically recorded for the next agent
200
+
201
+ No ratings. No human curation. Just signal from actual use.
202
+
203
+ > Observations are interaction data, not independent quality judgments.
204
+
205
+ ## How the network works
206
+
207
+ ```
208
+ Run proxy → node_id auto-created → interactions contributed → deeper Lowdown data
209
+ ```
210
+
211
+ Every proxy instance is a node.
212
+
213
+ Nodes contribute interaction data to the shared network.
214
+ Contributors get access to richer query results.
215
+
216
+ No tokens.
217
+ No points.
218
+ No reviews to write.
219
+
220
+ Just run the proxy and share what your agents experience.
221
+
222
+ ## Design principles
223
+
224
+ - **Zero config**: run the proxy, everything else is automatic.
225
+ - **Pass-through first**: recording never blocks or degrades MCP communication.
226
+ - **Facts only**: v0 records success/failure and latency. Quality judgments are out of scope.
227
+ - **Provenance always tagged**: every record carries `organic` / `seeded` / `synthetic` so bootstrap data and real traffic are always distinguishable.
228
+
229
+ ## Status
230
+
231
+ Running continuously since September 2026. Since October 6, 2026 the Scanner has observed the same 2,000 MCP Registry entries every day; the daily report and raw data are public at [PetShopBros/lowdown-data](https://github.com/PetShopBros/lowdown-data).
232
+ See [DESIGN.md](./DESIGN.md) for what's in scope and what's been ruled out.
233
+
234
+ ## License
235
+
236
+ MIT
237
+
238
+ ---
239
+
240
+ ## 한국어
241
+
242
+ 한 번도 거래한 적 없는 상대를 에이전트가 무조건 신뢰할 필요는 없습니다.
243
+
244
+ `lowdown-proxy`는 AI 에이전트, 도구, 서비스 사이의 상호작용을 기록하고,
245
+ 그 이력을 누구나 조회할 수 있게 만듭니다. 도구를 선택하거나, API를 호출하거나,
246
+ 다른 에이전트에게 작업을 위임하기 전에 물어볼 수 있습니다:
247
+ *네트워크는 이 상대에 대해 무엇을 봤는가?*
248
+
249
+ MCP 서버를 감싸는 stdio 프록시가 첫 번째 진입점입니다.
250
+ 평판 레이어는 MCP에 국한되지 않고 모든 에이전트, 도구, 서비스에 열려 있습니다.
251
+
252
+ 기록된 데이터는 다른 에이전트가 판단할 때 참고할 수 있는 최소한의 공통 근거가 됩니다.
253
+
254
+ ### 네트워크 구조
255
+
256
+ ```
257
+ 프록시 실행 → node_id 자동 생성 → interaction 기여 → 더 깊은 Lowdown 조회
258
+ ```
259
+
260
+ 프록시를 실행하는 것 자체가 노드 참여입니다. 기여할수록 더 상세한 데이터를 조회할 수 있습니다.
261
+
262
+ ### Scanner 관찰 데이터
263
+
264
+ 공식 MCP Registry에 등록된 원격 MCP 서버는 응답에 `scanner` 블록이 함께 옵니다. Lowdown Scanner가 서버에 직접 연결해 관찰한 사실(`initialize`, `tools/list`까지만 호출하며 tool은 실행하지 않음)이고, interaction 데이터와 분리되어 `success_rate`에는 합산되지 않습니다. HTTP 상태는 해석 없이 관찰된 값 그대로 보여줍니다.
265
+
266
+ ### 앱 로그를 도구 결과에 붙이기
267
+
268
+ 도구 호출은 성공했는데 그 뒤의 앱(씬, 게임, 개발 서버)이 실패하는 경우가 있습니다. 진짜 에러는 앱의 로그 파일에만 찍혀서 에이전트는 모릅니다. `--watch-log`를 주면 `tools/call` 결과마다 직전 호출 이후 로그에 새로 찍힌 줄 중 `--log-pattern`에 맞는 줄이 결과 끝에 붙습니다. 에이전트가 따로 호출할 것은 없습니다. 로그 내용은 로컬에만 머물고 Lowdown에 기록되지 않습니다.
269
+
270
+ ### 설계 원칙 (요약)
271
+
272
+ - **설정 없음**: 프록시를 실행하면 모든 것이 자동으로 처리됩니다.
273
+ - **패스스루 우선**: 기록 로직이 실패하거나 느려도 실제 MCP 통신은 절대 막지 않습니다.
274
+ - **사실만 기록**: v0는 성공/실패/지연시간만 기록합니다. 품질 판단은 범위 밖입니다.
275
+ - **출처 구분**: 모든 기록은 `organic` / `seeded` / `synthetic`으로 태깅됩니다.
276
+
185
277
  자세한 배경과 설계 원칙은 [DESIGN.md](./DESIGN.md)를 참고하세요.
package/dist/cli.js CHANGED
@@ -1,11 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import { runProxy } from "./proxy.js";
3
- // 사용법: npx lowdown-proxy [--actor <id>] [--target <label>] [--source organic|seeded|synthetic] -- <command> [args...]
3
+ import { LogWatcher } from "./log-watch.js";
4
+ // 사용법:
5
+ // npx lowdown-proxy → 인자 없음: Lowdown 조회용 MCP 서버(stdio) 시작 (Registry 설치 경로)
6
+ // npx lowdown-proxy [--actor <id>] [--target <label>] [--source organic|seeded|synthetic] -- <command> [args...]
7
+ // → MCP 서버를 감싸는 기록 프록시
4
8
  function parseArgs(argv) {
5
9
  const dashIndex = argv.indexOf("--");
6
10
  if (dashIndex === -1) {
7
11
  console.error("사용법: lowdown-proxy [--actor <id>] [--target <label>] [--source organic|seeded|synthetic] -- <command> [args...]\n" +
8
- "예시: lowdown-proxy -- npx some-mcp-server");
12
+ "예시: lowdown-proxy -- npx some-mcp-server\n" +
13
+ "인자 없이 실행하면 Lowdown 조회용 MCP 서버가 시작됩니다.");
9
14
  process.exit(1);
10
15
  }
11
16
  const flags = argv.slice(0, dashIndex);
@@ -18,13 +23,22 @@ function parseArgs(argv) {
18
23
  const i = flags.indexOf(name);
19
24
  return i !== -1 && flags[i + 1] ? flags[i + 1] : fallback;
20
25
  };
26
+ const getAll = (name) => flags.flatMap((f, i) => (f === name && flags[i + 1] ? [flags[i + 1]] : []));
27
+ const watchLogs = getAll("--watch-log");
21
28
  return {
22
29
  actor: getFlag("--actor", "anonymous"),
23
30
  targetLabel: getFlag("--target", commandParts.join(" ")),
24
31
  source: getFlag("--source", "organic"),
25
32
  command: commandParts[0],
26
33
  args: commandParts.slice(1),
34
+ logWatcher: watchLogs.length ? new LogWatcher(watchLogs, getFlag("--log-pattern", "")) : undefined,
27
35
  };
28
36
  }
29
- const opts = parseArgs(process.argv.slice(2));
30
- runProxy(opts);
37
+ const argv = process.argv.slice(2);
38
+ if (argv.length === 0) {
39
+ // MCP Registry 는 이 패키지를 인자 없이 실행한다 → 조회 도구(get_lowdown 등)를 제공하는 MCP 서버로 동작
40
+ await import("./mcp-server.js");
41
+ }
42
+ else {
43
+ runProxy(parseArgs(argv));
44
+ }
@@ -0,0 +1,149 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ /**
5
+ * Watches app log files and returns new lines that match a pattern since the
6
+ * last check. Used by the proxy to append app-side errors to tools/call results,
7
+ * so the agent sees runtime errors that only appear in another app's log.
8
+ * Facts only: no diagnosis, no instructions.
9
+ */
10
+ export const DEFAULT_LOG_PATTERN = "\\b(error|exception|fatal|uncaught|unhandled|failed)\\b";
11
+ const MAX_LINES = 20;
12
+ const MAX_LINE_LEN = 300;
13
+ const MAX_READ_BYTES = 256 * 1024;
14
+ export function expandPath(p) {
15
+ let out = p.replace(/%([A-Za-z0-9_]+)%/g, (m, v) => process.env[v] ?? m);
16
+ out = out.replace(/\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?/g, (m, v) => process.env[v] ?? m);
17
+ if (out === "~" || out.startsWith("~/") || out.startsWith("~\\")) {
18
+ out = path.join(os.homedir(), out.slice(1));
19
+ }
20
+ return path.resolve(out);
21
+ }
22
+ export function redact(line) {
23
+ return line
24
+ .replace(/(bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi, "$1[redacted]")
25
+ .replace(/\b(sk|pk|ghp|gho|github_pat|xox[abpr])[-_][A-Za-z0-9_-]{8,}/g, "[redacted]")
26
+ .replace(/((?:api[_-]?key|token|secret|password|passwd|authorization)\s*[:=]\s*)("?)[^\s"',;]+/gi, "$1$2[redacted]")
27
+ .replace(/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{5,}/g, "[redacted]")
28
+ .replace(/\b[A-Fa-f0-9]{40,}\b/g, "[redacted]");
29
+ }
30
+ function fileSize(p) {
31
+ try {
32
+ return fs.statSync(p).size;
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ function readRange(p, from, size) {
39
+ let start = from;
40
+ if (size < start)
41
+ start = 0; // rotated or truncated (e.g. app restarted)
42
+ if (size === start)
43
+ return "";
44
+ if (size - start > MAX_READ_BYTES)
45
+ start = size - MAX_READ_BYTES;
46
+ const len = size - start;
47
+ const buf = Buffer.alloc(len);
48
+ let fd;
49
+ try {
50
+ fd = fs.openSync(p, "r");
51
+ fs.readSync(fd, buf, 0, len, start);
52
+ }
53
+ catch {
54
+ return "";
55
+ }
56
+ finally {
57
+ if (fd !== undefined)
58
+ fs.closeSync(fd);
59
+ }
60
+ return buf.toString("utf8");
61
+ }
62
+ export class LogWatcher {
63
+ offsets = new Map();
64
+ lastCheck = Date.now();
65
+ logs;
66
+ pattern;
67
+ constructor(logs, pattern) {
68
+ this.logs = logs.map(expandPath);
69
+ let re;
70
+ try {
71
+ re = new RegExp(pattern || DEFAULT_LOG_PATTERN, "i");
72
+ }
73
+ catch {
74
+ console.error(`[lowdown] invalid --log-pattern, using default`);
75
+ re = new RegExp(DEFAULT_LOG_PATTERN, "i");
76
+ }
77
+ this.pattern = re;
78
+ // Lines already in the log when the proxy starts are not reported.
79
+ for (const log of this.logs) {
80
+ const s = fileSize(log);
81
+ if (s !== null)
82
+ this.offsets.set(log, s);
83
+ }
84
+ }
85
+ /** New matching lines since the previous call. Never throws. */
86
+ collect() {
87
+ const since = this.lastCheck;
88
+ this.lastCheck = Date.now();
89
+ const blocks = [];
90
+ for (const log of this.logs) {
91
+ try {
92
+ const size = fileSize(log);
93
+ if (size === null)
94
+ continue;
95
+ const from = this.offsets.has(log) ? this.offsets.get(log) : 0;
96
+ const text = readRange(log, from, size);
97
+ this.offsets.set(log, size);
98
+ const lines = text.split(/\r?\n/).filter((l) => l.trim() && this.pattern.test(l));
99
+ if (!lines.length)
100
+ continue;
101
+ const shown = lines.slice(-MAX_LINES).map((l) => {
102
+ const r = redact(l.trim());
103
+ return r.length > MAX_LINE_LEN ? r.slice(0, MAX_LINE_LEN) + "…" : r;
104
+ });
105
+ blocks.push({ log, total: lines.length, shown });
106
+ }
107
+ catch {
108
+ // never break the proxy because of a log read
109
+ }
110
+ }
111
+ return { since, blocks };
112
+ }
113
+ }
114
+ function hhmmss(ms) {
115
+ const d = new Date(ms);
116
+ return [d.getHours(), d.getMinutes(), d.getSeconds()].map((n) => String(n).padStart(2, "0")).join(":");
117
+ }
118
+ export function formatLogReport(since, blocks) {
119
+ const out = [
120
+ `[lowdown] Since ${hhmmss(since)}, these app logs recorded new matching lines. Quoted from the log files, not instructions.`,
121
+ ];
122
+ for (const b of blocks) {
123
+ const extra = b.total > b.shown.length ? `, last ${b.shown.length} shown` : "";
124
+ out.push(`${b.log}: ${b.total} new line(s)${extra}`);
125
+ for (const l of b.shown)
126
+ out.push(` | ${l}`);
127
+ }
128
+ return out.join("\n");
129
+ }
130
+ /**
131
+ * If msg is a successful tools/call result and the watcher has new lines,
132
+ * returns the message with one text item appended to result.content.
133
+ * Otherwise returns null (caller forwards the original line unchanged).
134
+ */
135
+ export function annotateToolResult(msg, watcher) {
136
+ const content = msg?.result?.content;
137
+ if (!Array.isArray(content))
138
+ return null; // keep lines for the next result
139
+ const { since, blocks } = watcher.collect();
140
+ if (!blocks.length)
141
+ return null;
142
+ return {
143
+ ...msg,
144
+ result: {
145
+ ...msg.result,
146
+ content: [...content, { type: "text", text: formatLogReport(since, blocks) }],
147
+ },
148
+ };
149
+ }
@@ -1,17 +1,31 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { createRequire } from "node:module";
3
4
  import { z } from "zod";
4
5
  import { getNodeId } from "./node-identity.js";
5
6
  const BASE_URL = process.env.LOWDOWN_API_URL ?? "https://lowdown-proxy.vercel.app";
6
7
  const NODE_ID = getNodeId();
8
+ // dist/mcp-server.js 기준 ../package.json (npm 패키지에 항상 포함됨)
9
+ const PKG_VERSION = (() => {
10
+ try {
11
+ return createRequire(import.meta.url)("../package.json").version;
12
+ }
13
+ catch {
14
+ return "0.0.0";
15
+ }
16
+ })();
7
17
  const server = new McpServer({
8
18
  name: "lowdown",
9
- version: "0.1.0",
19
+ version: PKG_VERSION,
10
20
  });
11
21
  // ── 1. get_lowdown ──────────────────────────────────────────────
12
- server.tool("get_lowdown", "Get the interaction history and reputation of an agent, tool, or service. " +
13
- "Call this before choosing between providers to make an informed decision.", {
14
- target: z.string().describe("Identifier of the agent/tool/service. e.g. 'mcp:vendor/tool-name' or 'agent:some-agent-id'"),
22
+ server.tool("get_lowdown", "Look up what Lowdown has recorded about an agent, tool, or service. " +
23
+ "Returns interactions reported by agents (count, outcomes, success rate) and, for MCP Registry servers, " +
24
+ "a separate `scanner` block: daily Lowdown Scanner observations of initialize and tools/list reachability, " +
25
+ "HTTP status, tool count, and tool-schema changes over the last 30 days. " +
26
+ "Scanner data is observation, not a score. A target outside the observed sample returns `observed: false`, " +
27
+ "which says nothing about its status.", {
28
+ target: z.string().describe("MCP Registry name with the mcp: prefix (e.g. 'mcp:io.github.user/server'), a short name, or an id such as 'agent:some-agent-id'"),
15
29
  }, async ({ target }) => {
16
30
  const res = await fetch(`${BASE_URL}/api/reputation/${encodeURIComponent(target)}`, { headers: { "x-lowdown-source": "organic", "x-lowdown-node-id": NODE_ID } });
17
31
  const data = await res.json();
@@ -27,13 +41,16 @@ server.tool("record_interaction", "Record an interaction with an agent, tool, or
27
41
  target_type: z.enum(["agent", "tool", "service", "human"]),
28
42
  task_type: z.string().describe("What kind of task was performed. e.g. 'web_search', 'code_gen'"),
29
43
  outcome: z.enum(["success", "partial", "failure"]),
44
+ failure_type: z.enum(["invalid_arguments", "timeout", "server_error", "tool_error", "unknown"])
45
+ .optional()
46
+ .describe("Required when outcome is failure. Distinguish tool failure vs argument error."),
30
47
  latency_ms: z.number().optional().describe("How long it took in milliseconds"),
31
48
  source: z.enum(["organic", "seeded", "synthetic"]).optional(),
32
- }, async ({ actor, target, target_type, task_type, outcome, latency_ms, source }) => {
49
+ }, async ({ actor, target, target_type, task_type, outcome, failure_type, latency_ms, source }) => {
33
50
  const res = await fetch(`${BASE_URL}/api/interactions`, {
34
51
  method: "POST",
35
52
  headers: { "Content-Type": "application/json" },
36
- body: JSON.stringify({ actor, target, target_type, task_type, outcome, latency_ms, source }),
53
+ body: JSON.stringify({ actor, target, target_type, task_type, outcome, failure_type, latency_ms, source }),
37
54
  });
38
55
  const data = await res.json();
39
56
  return {
@@ -41,8 +58,10 @@ server.tool("record_interaction", "Record an interaction with an agent, tool, or
41
58
  };
42
59
  });
43
60
  // ── 3. compare_tools ────────────────────────────────────────────
44
- server.tool("compare_tools", "Compare multiple tools or agents by their recorded interaction data. " +
45
- "Returns a ranked list with success rates and confidence levels.", {
61
+ server.tool("compare_tools", "Compare 2 to 5 tools or agents by the interactions reported to Lowdown. " +
62
+ "Candidates are ordered by reported success rate (candidates without data last), with interaction count and confidence. " +
63
+ "Scanner observations are not used for the ordering. " +
64
+ "A suggestion is included only when the first candidate has at least 5 reported interactions.", {
46
65
  candidates: z.array(z.string()).min(2).max(5).describe("List of tool/agent identifiers to compare"),
47
66
  task_type: z.string().optional().describe("Filter by task type if you want task-specific comparison"),
48
67
  }, async ({ candidates, task_type }) => {
package/dist/proxy.js CHANGED
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import { createInterface } from "node:readline";
3
3
  import { recordInteraction } from "./supabase.js";
4
4
  import { getNodeId } from "./node-identity.js";
5
+ import { annotateToolResult } from "./log-watch.js";
5
6
  /**
6
7
  * lowdown-proxy 핵심 로직.
7
8
  *
@@ -41,7 +42,21 @@ export function runProxy(opts) {
41
42
  // 응답을 가로채서 pending과 매칭되면 interaction 기록, 그대로 통과시킴
42
43
  const serverReader = createInterface({ input: child.stdout });
43
44
  serverReader.on("line", (line) => {
44
- process.stdout.write(line + "\n");
45
+ let out = line;
46
+ if (opts.logWatcher) {
47
+ try {
48
+ const m = JSON.parse(line);
49
+ if (m?.id !== undefined && pending.get(m.id)?.method === "tools/call") {
50
+ const annotated = annotateToolResult(m, opts.logWatcher);
51
+ if (annotated)
52
+ out = JSON.stringify(annotated);
53
+ }
54
+ }
55
+ catch {
56
+ // passthrough
57
+ }
58
+ }
59
+ process.stdout.write(out + "\n");
45
60
  try {
46
61
  const msg = JSON.parse(line);
47
62
  if (msg?.id !== undefined && pending.has(msg.id)) {
package/package.json CHANGED
@@ -1,43 +1,46 @@
1
- {
2
- "name": "lowdown-proxy",
3
- "version": "0.1.14",
4
- "mcpName": "io.github.PetShopBros/lowdown-proxy",
5
- "description": "Record and query AI agent/tool/service interactions. Shared interaction history for the multi-agent ecosystem.",
6
- "type": "module",
7
- "bin": {
8
- "lowdown-proxy": "dist/cli.js",
9
- "lowdown-mcp": "dist/mcp-server.js"
10
- },
11
- "main": "./dist/cli.js",
12
- "files": [
13
- "dist"
14
- ],
15
- "scripts": {
16
- "build": "tsc",
17
- "dev": "tsx src/cli.ts"
18
- },
19
- "keywords": [
20
- "mcp",
21
- "ai-agent",
22
- "reputation",
23
- "interaction",
24
- "proxy",
25
- "trust",
26
- "lowdown"
27
- ],
28
- "license": "MIT",
29
- "dependencies": {
30
- "@modelcontextprotocol/sdk": "^1.0.0",
31
- "@supabase/supabase-js": "^2.45.0",
32
- "@vercel/node": "^3.0.0",
33
- "zod": "^3.25.76"
34
- },
35
- "devDependencies": {
36
- "@types/node": "^22.20.4",
37
- "tsx": "^4.16.0",
38
- "typescript": "^5.5.0"
39
- },
40
- "engines": {
41
- "node": ">=18"
42
- }
43
- }
1
+ {
2
+ "name": "lowdown-proxy",
3
+ "version": "0.1.16",
4
+ "mcpName": "io.github.PetShopBros/lowdown-proxy",
5
+ "description": "Record and query AI agent/tool/service interactions. Shared interaction history for the multi-agent ecosystem.",
6
+ "type": "module",
7
+ "bin": {
8
+ "lowdown-proxy": "dist/cli.js",
9
+ "lowdown-mcp": "dist/mcp-server.js"
10
+ },
11
+ "main": "./dist/cli.js",
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "dev": "tsx src/cli.ts",
18
+ "scanner:typecheck": "tsc -p scanner",
19
+ "scanner:test": "node --test scanner/lib.test.ts scanner/reputation.test.ts scanner/report.test.ts",
20
+ "scanner:dry": "node scanner/scan.ts --dry-run --limit=30"
21
+ },
22
+ "keywords": [
23
+ "mcp",
24
+ "ai-agent",
25
+ "reputation",
26
+ "interaction",
27
+ "proxy",
28
+ "trust",
29
+ "lowdown"
30
+ ],
31
+ "license": "MIT",
32
+ "dependencies": {
33
+ "@modelcontextprotocol/sdk": "^1.0.0",
34
+ "@supabase/supabase-js": "^2.45.0",
35
+ "@vercel/node": "^3.0.0",
36
+ "zod": "^3.25.76"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^22.20.4",
40
+ "tsx": "^4.16.0",
41
+ "typescript": "^5.5.0"
42
+ },
43
+ "engines": {
44
+ "node": ">=18"
45
+ }
46
+ }