lowdown-proxy 0.1.22 → 0.1.23
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 +8 -7
- package/dist/mcp-server.js +104 -45
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://mcpservers.org/servers/petshopbros/lowdown-proxy)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Lowdown records dated facts about public MCP servers before you install or connect them:
|
|
6
|
+
whether each server answered `initialize` and `tools/list` on daily checks, what the official
|
|
7
|
+
MCP Registry and the server's own cards declare, the state of its npm and PyPI packages,
|
|
8
|
+
when its tool definitions changed, and the payment terms it states. Facts, not scores.
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
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
|
+
Ask it three ways: the remote MCP server at `https://lowdown-proxy.vercel.app/mcp`
|
|
11
|
+
(`get_lowdown`, `find_servers`), the HTTP API, or the open daily dataset (CC BY 4.0).
|
|
12
|
+
`lowdown-proxy` also wraps a stdio MCP server and adds this record to its `initialize` response.
|
|
12
13
|
|
|
13
14
|
See [DESIGN.md](./DESIGN.md) for background and design principles.
|
|
14
15
|
|
|
@@ -342,4 +343,4 @@ PyPI 패키지도 같은 `packages` 블록에 `pypi` 상태로 옵니다. PyPI
|
|
|
342
343
|
- **사실만 기록**: v0는 성공/실패/지연시간만 기록합니다. 품질 판단은 범위 밖입니다.
|
|
343
344
|
- **출처 구분**: 모든 기록은 `organic` / `seeded` / `synthetic`으로 태깅됩니다.
|
|
344
345
|
|
|
345
|
-
자세한 배경과 설계 원칙은 [DESIGN.md](./DESIGN.md)를 참고하세요.
|
|
346
|
+
자세한 배경과 설계 원칙은 [DESIGN.md](./DESIGN.md)를 참고하세요.
|
package/dist/mcp-server.js
CHANGED
|
@@ -18,50 +18,94 @@ const server = new McpServer({
|
|
|
18
18
|
name: "lowdown",
|
|
19
19
|
version: PKG_VERSION,
|
|
20
20
|
});
|
|
21
|
+
// 오류 응답(2026-11-15): 원격 MCP(api/mcp.ts toolError·upstreamError)와 같은 모양 { error, message, retryable, http_status? }.
|
|
22
|
+
// 조회 API 가 표준 오류 JSON 을 주면 그대로 넘기고, 연결 실패·JSON 아닌 응답도 isError 로 알린다(예외로 끝나지 않게).
|
|
23
|
+
function toolError(body) {
|
|
24
|
+
return { isError: true, content: [{ type: "text", text: JSON.stringify(body) }] };
|
|
25
|
+
}
|
|
26
|
+
async function callApi(url, init, what) {
|
|
27
|
+
let res;
|
|
28
|
+
try {
|
|
29
|
+
res = await fetch(url, { ...init, signal: AbortSignal.timeout(20_000) });
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return { ok: false, result: toolError({ error: "temporarily_unavailable", message: `Lowdown ${what} did not answer. Retry later.`, retryable: true }) };
|
|
33
|
+
}
|
|
34
|
+
const text = await res.text();
|
|
35
|
+
let data = null;
|
|
36
|
+
try {
|
|
37
|
+
data = JSON.parse(text);
|
|
38
|
+
}
|
|
39
|
+
catch { /* JSON 아님 */ }
|
|
40
|
+
if (!res.ok || data === null) {
|
|
41
|
+
const base = data && typeof data === "object" && typeof data.error === "string" ? data : {
|
|
42
|
+
error: res.status === 429 ? "rate_limited" : res.status >= 500 ? "temporarily_unavailable" : "invalid_parameter",
|
|
43
|
+
message: `Lowdown ${what} answered HTTP ${res.status}.`,
|
|
44
|
+
};
|
|
45
|
+
return { ok: false, result: toolError({ ...base, retryable: typeof base.retryable === "boolean" ? base.retryable : res.status === 429 || res.status >= 500, http_status: res.status }) };
|
|
46
|
+
}
|
|
47
|
+
return { ok: true, data };
|
|
48
|
+
}
|
|
49
|
+
// annotations(2026-11-15): 원격 MCP 와 같은 기준. 쓰기는 record_interaction 하나뿐(Lowdown 기록에 한 줄 추가, 지우거나 바꾸지 않음).
|
|
50
|
+
const READ_ONLY = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
21
51
|
// ── 1. get_lowdown ──────────────────────────────────────────────
|
|
22
52
|
// 메뉴 목록은 원격 MCP(api/mcp.ts MENUS)·조회 API(api/reputation/[target].ts DISHES·SETS)와 같아야 한다.
|
|
23
53
|
const MENUS = [
|
|
24
54
|
"install-check", "change-watch", "full",
|
|
25
55
|
"status", "payment", "packages", "tools", "declared-vs-observed", "content-flags", "tool-list", "registry", "history", "agent-reports",
|
|
26
56
|
];
|
|
27
|
-
server.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"
|
|
57
|
+
server.registerTool("get_lowdown", {
|
|
58
|
+
title: "Look up recorded facts about an MCP server",
|
|
59
|
+
description: "Look up recorded facts about an MCP server before installing or connecting to it. " +
|
|
60
|
+
"Accepts an MCP Registry name ('mcp:io.github.user/server'), a short name, or a package ('npm:@scope/name', 'pypi:name'). " +
|
|
61
|
+
"Returns dated facts, not scores: `status` (latest daily initialize/tools/list check: answered, required authentication, required payment, or no response), " +
|
|
62
|
+
"`payment` (HTTP 402 payment terms stated on the latest check — x402, MPP or L402 — and payment support declared in the server's card or an ERC-8004 registration file; Lowdown does not pay), " +
|
|
63
|
+
"`registry` (what the official MCP Registry declares, and changes in the last 30 days), " +
|
|
64
|
+
"`packages` (declared npm/PyPI packages: deprecated, yanked, archived, or 404), " +
|
|
65
|
+
"`scanner` (when tool definitions changed and content markers such as a new URL or invisible characters), and agent reports. " +
|
|
66
|
+
"The optional `menu` returns only one part, e.g. 'install-check' (the facts used before installing). " +
|
|
67
|
+
"Absence of data says nothing about a server's status. " +
|
|
68
|
+
"When Lowdown has no record for the name, the answer says so in `message` (with close Registry names in `registry_candidates` when there are any); it is not an error. " +
|
|
69
|
+
"If Lowdown cannot read its records, the tool returns an error with `retryable: true`. Read-only, no authentication, no tool of the looked-up server is called.",
|
|
70
|
+
inputSchema: {
|
|
71
|
+
target: z.string().min(1).max(300).describe("MCP Registry name with the mcp: prefix (e.g. 'mcp:io.github.user/server'), a short name, or a package with the npm: or pypi: prefix"),
|
|
72
|
+
menu: z.enum(MENUS).optional().describe("Optional. Before installing, use 'install-check' (status, payment, packages, declared-vs-observed, content-flags). 'change-watch' = what changed recently, " +
|
|
73
|
+
"or one part: status, payment, packages, tools, declared-vs-observed, content-flags, tool-list, registry, history, agent-reports. Omit or 'full' for everything."),
|
|
74
|
+
},
|
|
75
|
+
annotations: { title: "Look up recorded facts about an MCP server", ...READ_ONLY },
|
|
39
76
|
}, async ({ target, menu }) => {
|
|
40
77
|
const q = menu && menu !== "full" ? `?menu=${encodeURIComponent(menu)}` : "";
|
|
41
|
-
const
|
|
42
|
-
|
|
78
|
+
const r = await callApi(`${BASE_URL}/api/reputation/${encodeURIComponent(target.trim())}${q}`, { headers: { "x-lowdown-source": "organic", "x-lowdown-node-id": NODE_ID } }, "lookup");
|
|
79
|
+
if (!r.ok)
|
|
80
|
+
return r.result;
|
|
43
81
|
return {
|
|
44
|
-
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
82
|
+
content: [{ type: "text", text: JSON.stringify(r.data, null, 2) }],
|
|
45
83
|
};
|
|
46
84
|
});
|
|
47
85
|
// ── 2. record_interaction ───────────────────────────────────────
|
|
48
|
-
server.
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
.
|
|
59
|
-
.describe("
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
86
|
+
server.registerTool("record_interaction", {
|
|
87
|
+
title: "Report how a tool or agent worked for you",
|
|
88
|
+
description: "Record an interaction with an agent, tool, or service after using it. " +
|
|
89
|
+
"Helps build the shared reputation dataset for the community. " +
|
|
90
|
+
"If the call was an intentional negative test (e.g. deliberately invalid input), set source to 'synthetic' " +
|
|
91
|
+
"so it is not counted as a failure of the target. " +
|
|
92
|
+
"Writes one report to Lowdown (kept apart from Scanner observations); nothing is changed or deleted, and each call adds a new report.",
|
|
93
|
+
inputSchema: {
|
|
94
|
+
actor: z.string().describe("Your agent/system identifier"),
|
|
95
|
+
target: z.string().describe("Identifier of what you used"),
|
|
96
|
+
target_type: z.enum(["agent", "tool", "service", "human"]),
|
|
97
|
+
task_type: z.string().describe("What kind of task was performed. e.g. 'web_search', 'code_gen'"),
|
|
98
|
+
outcome: z.enum(["success", "partial", "failure"]),
|
|
99
|
+
failure_type: z.enum(["invalid_arguments", "timeout", "server_error", "tool_error", "unknown"])
|
|
100
|
+
.optional()
|
|
101
|
+
.describe("Required when outcome is failure. Distinguish tool failure vs argument error."),
|
|
102
|
+
latency_ms: z.number().optional().describe("How long it took in milliseconds"),
|
|
103
|
+
source: z.enum(["organic", "seeded", "synthetic"]).optional()
|
|
104
|
+
.describe("organic (default) for real use; synthetic for tests, including intentional failure tests"),
|
|
105
|
+
},
|
|
106
|
+
annotations: { title: "Report how a tool or agent worked for you", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
63
107
|
}, async ({ actor, target, target_type, task_type, outcome, failure_type, latency_ms, source }) => {
|
|
64
|
-
const
|
|
108
|
+
const r = await callApi(`${BASE_URL}/api/interactions`, {
|
|
65
109
|
method: "POST",
|
|
66
110
|
headers: { "Content-Type": "application/json" },
|
|
67
111
|
body: JSON.stringify({
|
|
@@ -70,19 +114,28 @@ server.tool("record_interaction", "Record an interaction with an agent, tool, or
|
|
|
70
114
|
node_id: NODE_ID,
|
|
71
115
|
recorder_version: PKG_VERSION,
|
|
72
116
|
}),
|
|
73
|
-
});
|
|
74
|
-
|
|
117
|
+
}, "report");
|
|
118
|
+
if (!r.ok)
|
|
119
|
+
return r.result;
|
|
120
|
+
if (!r.data?.ok)
|
|
121
|
+
return toolError({ error: String(r.data?.error ?? "not_recorded"), message: "Lowdown did not record the report.", retryable: false });
|
|
75
122
|
return {
|
|
76
|
-
content: [{ type: "text", text:
|
|
123
|
+
content: [{ type: "text", text: "Recorded." }],
|
|
77
124
|
};
|
|
78
125
|
});
|
|
79
126
|
// ── 3. compare_tools ────────────────────────────────────────────
|
|
80
|
-
server.
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
127
|
+
server.registerTool("compare_tools", {
|
|
128
|
+
title: "Compare tools by reported outcomes",
|
|
129
|
+
description: "Compare 2 to 5 tools or agents by the interactions reported to Lowdown. " +
|
|
130
|
+
"Candidates are ordered by reported success rate (candidates without data last), with interaction count and confidence. " +
|
|
131
|
+
"Scanner observations are not used for the ordering. " +
|
|
132
|
+
"A suggestion is included only when the first candidate has at least 5 rated interactions. " +
|
|
133
|
+
"A candidate Lowdown cannot read is listed with no data rather than failing the whole comparison. Read-only.",
|
|
134
|
+
inputSchema: {
|
|
135
|
+
candidates: z.array(z.string()).min(2).max(5).describe("List of tool/agent identifiers to compare"),
|
|
136
|
+
task_type: z.string().optional().describe("Filter by task type if you want task-specific comparison"),
|
|
137
|
+
},
|
|
138
|
+
annotations: { title: "Compare tools by reported outcomes", ...READ_ONLY },
|
|
86
139
|
}, async ({ candidates, task_type }) => {
|
|
87
140
|
// compare_calls 로그 (fire-and-forget)
|
|
88
141
|
void fetch(`${BASE_URL}/api/compare-log`, {
|
|
@@ -144,12 +197,18 @@ server.tool("compare_tools", "Compare 2 to 5 tools or agents by the interactions
|
|
|
144
197
|
};
|
|
145
198
|
});
|
|
146
199
|
// ── 4. get_node_stats ───────────────────────────────────────────
|
|
147
|
-
server.
|
|
148
|
-
"
|
|
149
|
-
|
|
150
|
-
|
|
200
|
+
server.registerTool("get_node_stats", {
|
|
201
|
+
title: "Show this proxy node's reports",
|
|
202
|
+
description: "Get your node's contribution stats and network position. " +
|
|
203
|
+
"Shows how much your proxy has contributed to the Lowdown network. " +
|
|
204
|
+
"The node is the local identity in ~/.lowdown/node.json; no input is needed. Read-only.",
|
|
205
|
+
annotations: { title: "Show this proxy node's reports", ...READ_ONLY },
|
|
206
|
+
}, async () => {
|
|
207
|
+
const r = await callApi(`${BASE_URL}/api/node/${encodeURIComponent(NODE_ID)}`, { headers: { "x-lowdown-source": "organic" } }, "node stats");
|
|
208
|
+
if (!r.ok)
|
|
209
|
+
return r.result;
|
|
151
210
|
return {
|
|
152
|
-
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
211
|
+
content: [{ type: "text", text: JSON.stringify(r.data, null, 2) }],
|
|
153
212
|
};
|
|
154
213
|
});
|
|
155
214
|
// success_rate 의 분모(organic 도구 호출·보고). 0.1.17 이전 API 응답에는 없으므로 interactions 로 대체.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lowdown-proxy",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
4
4
|
"mcpName": "io.github.PetShopBros/lowdown-proxy",
|
|
5
5
|
"description": "Check an MCP server before you install it: dated facts from daily initialize/tools/list checks of public MCP servers, MCP Registry declarations, npm/PyPI package state, tool-definition changes and payment terms (x402, MPP, L402). Remote MCP and stdio. No scores.",
|
|
6
6
|
"type": "module",
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
"scanner:typecheck": "tsc -p scanner",
|
|
19
19
|
"sql:baseline": "node scanner/migrations.ts baseline",
|
|
20
20
|
"sql:check": "node scanner/migrations.ts check",
|
|
21
|
-
"scanner:test": "node --test scanner/lib.test.ts scanner/registry-diff.test.ts scanner/first-seen.test.ts scanner/archive.test.ts scanner/reputation.test.ts scanner/report.test.ts scanner/tool-defs.test.ts scanner/market.test.ts scanner/private-archive.test.ts scanner/requested.test.ts scanner/sandbox-public.test.ts scanner/npm.test.ts scanner/pypi.test.ts scanner/pages.test.ts scanner/unavailable.test.ts scanner/target-status.test.ts scanner/content-flags.test.ts scanner/page.test.ts scanner/cards.test.ts scanner/mcp-remote.test.ts scanner/find.test.ts scanner/no-bom.test.ts scanner/status.test.ts scanner/sitemap.test.ts scanner/schema.test.ts scanner/agents.test.ts scanner/server-card.test.ts scanner/declared.test.ts scanner/erc8004.test.ts scanner/card-discovery.test.ts scanner/rotating.test.ts scanner/smoke.test.ts scanner/migrations.test.ts scanner/ops.test.ts scanner/monthly.test.ts scanner/payment.test.ts scanner/attest.test.ts",
|
|
21
|
+
"scanner:test": "node --test scanner/lib.test.ts scanner/registry-diff.test.ts scanner/first-seen.test.ts scanner/archive.test.ts scanner/reputation.test.ts scanner/report.test.ts scanner/tool-defs.test.ts scanner/market.test.ts scanner/private-archive.test.ts scanner/requested.test.ts scanner/sandbox-public.test.ts scanner/npm.test.ts scanner/pypi.test.ts scanner/pages.test.ts scanner/unavailable.test.ts scanner/target-status.test.ts scanner/content-flags.test.ts scanner/page.test.ts scanner/cards.test.ts scanner/mcp-remote.test.ts scanner/find.test.ts scanner/no-bom.test.ts scanner/status.test.ts scanner/sitemap.test.ts scanner/schema.test.ts scanner/agents.test.ts scanner/server-card.test.ts scanner/declared.test.ts scanner/erc8004.test.ts scanner/card-discovery.test.ts scanner/rotating.test.ts scanner/smoke.test.ts scanner/migrations.test.ts scanner/ops.test.ts scanner/monthly.test.ts scanner/payment.test.ts scanner/attest.test.ts scanner/access-report.test.ts",
|
|
22
22
|
"scanner:dry": "node scanner/scan.ts --dry-run --limit=30",
|
|
23
|
-
"test": "npm run build && node --test test/p0.test.ts test/lookup.test.ts",
|
|
23
|
+
"test": "npm run build && node --test test/p0.test.ts test/lookup.test.ts test/mcp-server.test.ts",
|
|
24
24
|
"sandbox:test": "node --test sandbox/sandbox.test.ts sandbox/observe.test.ts",
|
|
25
25
|
"sandbox:typecheck": "tsc -p sandbox"
|
|
26
26
|
},
|