lowdown-proxy 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.
- package/LICENSE +21 -0
- package/README.md +74 -0
- package/dist/cli.js +30 -0
- package/dist/proxy.js +73 -0
- package/dist/supabase.js +50 -0
- package/package.json +30 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lowdown
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# lowdown-proxy
|
|
2
|
+
|
|
3
|
+
Agents should not have to trust an agent/tool they've never interacted with.
|
|
4
|
+
|
|
5
|
+
`lowdown-proxy`는 MCP 클라이언트와 서버 사이를 투명하게 통과하는 stdio 프록시입니다.
|
|
6
|
+
통신 내용은 그대로 전달하면서, 동시에 어떤 도구가 호출됐고 성공/실패했는지, 얼마나
|
|
7
|
+
걸렸는지를 자동으로 기록합니다.
|
|
8
|
+
|
|
9
|
+
기록된 데이터는 다른 에이전트가 "이 도구를 써도 되는가"를 판단할 때 참고할 수 있는
|
|
10
|
+
최소한의 공통 근거가 됩니다.
|
|
11
|
+
|
|
12
|
+
자세한 배경과 설계 원칙은 [DESIGN.md](./DESIGN.md)를 참고하세요.
|
|
13
|
+
|
|
14
|
+
## 설치 없이 실행
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx lowdown-proxy -- npx some-mcp-server
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
기존 MCP 클라이언트 설정에서 서버를 감싸기만 하면 됩니다.
|
|
21
|
+
|
|
22
|
+
```jsonc
|
|
23
|
+
// 기존
|
|
24
|
+
{
|
|
25
|
+
"mcpServers": {
|
|
26
|
+
"search": { "command": "npx", "args": ["search-mcp-server"] }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// lowdown-proxy로 감싼 뒤
|
|
31
|
+
{
|
|
32
|
+
"mcpServers": {
|
|
33
|
+
"search": {
|
|
34
|
+
"command": "npx",
|
|
35
|
+
"args": ["lowdown-proxy", "--target", "search-mcp-server", "--", "npx", "search-mcp-server"]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## 옵션
|
|
42
|
+
|
|
43
|
+
| 플래그 | 설명 | 기본값 |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `--actor` | 호출 주체 식별자 | `anonymous` |
|
|
46
|
+
| `--target` | 기록될 대상 이름 (도구 식별자) | 실행 명령어 문자열 |
|
|
47
|
+
| `--source` | `organic` \| `seeded` \| `synthetic` | `organic` |
|
|
48
|
+
|
|
49
|
+
## 환경변수
|
|
50
|
+
|
|
51
|
+
기록 기능을 쓰려면 아래 두 값이 필요합니다. 없어도 프록시는 정상 동작합니다
|
|
52
|
+
(패스스루가 최우선, 기록은 부가 기능).
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
export LOWDOWN_SUPABASE_URL="https://xxxx.supabase.co"
|
|
56
|
+
export LOWDOWN_SUPABASE_KEY="..."
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 설계 원칙 (요약)
|
|
60
|
+
|
|
61
|
+
- **패스스루 우선**: 기록 로직이 실패하거나 느려도 실제 MCP 통신은 절대 막지 않습니다.
|
|
62
|
+
- **사실만 기록**: v0는 성공/실패/지연시간만 기록합니다. 품질 판단(별점 등)은 범위 밖입니다.
|
|
63
|
+
- **출처 구분**: 모든 기록은 `organic`(자연 유입) / `seeded`(직접 심은 트래픽) /
|
|
64
|
+
`synthetic`(합성 벤치마크)으로 태깅됩니다. 나중에 "실제 수요가 있었는가"를 검증할 때
|
|
65
|
+
이 구분이 핵심 근거가 됩니다.
|
|
66
|
+
|
|
67
|
+
## 상태
|
|
68
|
+
|
|
69
|
+
이 프로젝트는 2026년 9월 시작된 **30일 공개 실험**입니다. 확장 계획과 소거된 방향들은
|
|
70
|
+
[DESIGN.md](./DESIGN.md)에 정리되어 있습니다.
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
MIT
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runProxy } from "./proxy.js";
|
|
3
|
+
// 사용법: npx lowdown-proxy [--actor <id>] [--target <label>] [--source organic|seeded|synthetic] -- <command> [args...]
|
|
4
|
+
function parseArgs(argv) {
|
|
5
|
+
const dashIndex = argv.indexOf("--");
|
|
6
|
+
if (dashIndex === -1) {
|
|
7
|
+
console.error("사용법: lowdown-proxy [--actor <id>] [--target <label>] [--source organic|seeded|synthetic] -- <command> [args...]\n" +
|
|
8
|
+
"예시: lowdown-proxy -- npx some-mcp-server");
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
const flags = argv.slice(0, dashIndex);
|
|
12
|
+
const commandParts = argv.slice(dashIndex + 1);
|
|
13
|
+
if (commandParts.length === 0) {
|
|
14
|
+
console.error("[lowdown] '--' 뒤에 실행할 명령어가 없습니다.");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
const getFlag = (name, fallback) => {
|
|
18
|
+
const i = flags.indexOf(name);
|
|
19
|
+
return i !== -1 && flags[i + 1] ? flags[i + 1] : fallback;
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
actor: getFlag("--actor", "anonymous"),
|
|
23
|
+
targetLabel: getFlag("--target", commandParts.join(" ")),
|
|
24
|
+
source: getFlag("--source", "organic"),
|
|
25
|
+
command: commandParts[0],
|
|
26
|
+
args: commandParts.slice(1),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
30
|
+
runProxy(opts);
|
package/dist/proxy.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import { recordInteraction } from "./supabase.js";
|
|
4
|
+
/**
|
|
5
|
+
* lowdown-proxy 핵심 로직.
|
|
6
|
+
*
|
|
7
|
+
* 설계 원칙 (lowdown-brief.md 참고):
|
|
8
|
+
* - 패스스루가 최우선. 기록 로직이 통신을 막거나 지연시켜선 안 된다.
|
|
9
|
+
* - "무슨 일이 일어났는가"만 기록한다(Interaction 레이어). 품질 판단(Opinion)은 v0 범위 밖.
|
|
10
|
+
* - tool_execution 성공(JSON-RPC 에러 없음)과 실제 결과 품질은 다른 문제 — v0는 전자만 다룬다.
|
|
11
|
+
*/
|
|
12
|
+
export function runProxy(opts) {
|
|
13
|
+
const child = spawn(opts.command, opts.args, {
|
|
14
|
+
stdio: ["pipe", "pipe", "inherit"], // stderr는 그대로 상위로 흘려보냄(디버깅용)
|
|
15
|
+
});
|
|
16
|
+
const pending = new Map();
|
|
17
|
+
// ── Client(부모 프로세스의 stdin) → Server(child.stdin) ──
|
|
18
|
+
// 요청을 가로채서 tools/call이면 pending에 기록해두고, 그대로 통과시킴
|
|
19
|
+
const clientReader = createInterface({ input: process.stdin });
|
|
20
|
+
clientReader.on("line", (line) => {
|
|
21
|
+
// 항상 먼저 그대로 전달 — 파싱 실패해도 패스스루는 깨지면 안 됨
|
|
22
|
+
child.stdin.write(line + "\n");
|
|
23
|
+
try {
|
|
24
|
+
const msg = JSON.parse(line);
|
|
25
|
+
if (msg?.id !== undefined && msg?.method) {
|
|
26
|
+
pending.set(msg.id, {
|
|
27
|
+
method: msg.method,
|
|
28
|
+
toolName: msg.method === "tools/call" ? msg.params?.name : undefined,
|
|
29
|
+
startedAt: Date.now(),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// JSON이 아니거나 파싱 실패 — 무시하고 패스스루만 유지
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
// ── Server(child.stdout) → Client(부모 프로세스의 stdout) ──
|
|
38
|
+
// 응답을 가로채서 pending과 매칭되면 interaction 기록, 그대로 통과시킴
|
|
39
|
+
const serverReader = createInterface({ input: child.stdout });
|
|
40
|
+
serverReader.on("line", (line) => {
|
|
41
|
+
process.stdout.write(line + "\n");
|
|
42
|
+
try {
|
|
43
|
+
const msg = JSON.parse(line);
|
|
44
|
+
if (msg?.id !== undefined && pending.has(msg.id)) {
|
|
45
|
+
const call = pending.get(msg.id);
|
|
46
|
+
pending.delete(msg.id);
|
|
47
|
+
const latency_ms = Date.now() - call.startedAt;
|
|
48
|
+
const outcome = msg.error
|
|
49
|
+
? "failure"
|
|
50
|
+
: "success";
|
|
51
|
+
const record = {
|
|
52
|
+
actor: opts.actor,
|
|
53
|
+
target: opts.targetLabel,
|
|
54
|
+
target_type: "tool",
|
|
55
|
+
task_type: call.toolName ?? call.method,
|
|
56
|
+
outcome,
|
|
57
|
+
latency_ms,
|
|
58
|
+
source: opts.source,
|
|
59
|
+
};
|
|
60
|
+
// fire-and-forget — 응답 전달을 기다리게 하지 않음
|
|
61
|
+
void recordInteraction(record);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// 무시, 패스스루 유지
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
child.on("exit", (code) => {
|
|
69
|
+
process.exit(code ?? 0);
|
|
70
|
+
});
|
|
71
|
+
process.on("SIGINT", () => child.kill("SIGINT"));
|
|
72
|
+
process.on("SIGTERM", () => child.kill("SIGTERM"));
|
|
73
|
+
}
|
package/dist/supabase.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createClient } from "@supabase/supabase-js";
|
|
2
|
+
let client = null;
|
|
3
|
+
function getClient() {
|
|
4
|
+
if (client)
|
|
5
|
+
return client;
|
|
6
|
+
const url = process.env.LOWDOWN_SUPABASE_URL;
|
|
7
|
+
const key = process.env.LOWDOWN_SUPABASE_KEY;
|
|
8
|
+
if (!url || !key) {
|
|
9
|
+
// 설계 원칙: 자격증명이 없어도 프록시 자체는 죽지 않는다.
|
|
10
|
+
// 패스스루가 우선이고, 기록은 부가 기능이다.
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
client = createClient(url, key, { auth: { persistSession: false } });
|
|
14
|
+
return client;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* interaction을 Supabase에 기록한다.
|
|
18
|
+
* 절대 throw하지 않는다 — 기록 실패가 프록시 통신을 막아서는 안 된다.
|
|
19
|
+
* (기록은 부가 기능, 패스스루가 본 기능)
|
|
20
|
+
*/
|
|
21
|
+
export async function recordInteraction(record) {
|
|
22
|
+
const supabase = getClient();
|
|
23
|
+
if (!supabase) {
|
|
24
|
+
if (process.env.LOWDOWN_DEBUG) {
|
|
25
|
+
console.error("[lowdown] LOWDOWN_SUPABASE_URL/KEY 미설정 — 기록 건너뜀:", record);
|
|
26
|
+
}
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
// Database 타입을 별도 생성하지 않은 v0 상태라 insert 페이로드는 any로 캐스팅.
|
|
31
|
+
// (나중에 `supabase gen types`로 타입을 뽑으면 제거 가능)
|
|
32
|
+
const { error } = await supabase.from("interactions").insert({
|
|
33
|
+
actor: record.actor,
|
|
34
|
+
target: record.target,
|
|
35
|
+
target_type: record.target_type,
|
|
36
|
+
task_type: record.task_type,
|
|
37
|
+
outcome: record.outcome,
|
|
38
|
+
latency_ms: record.latency_ms ?? null,
|
|
39
|
+
source: record.source ?? "organic",
|
|
40
|
+
});
|
|
41
|
+
if (error && process.env.LOWDOWN_DEBUG) {
|
|
42
|
+
console.error("[lowdown] interaction 기록 실패:", error.message);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
if (process.env.LOWDOWN_DEBUG) {
|
|
47
|
+
console.error("[lowdown] interaction 기록 예외:", err);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lowdown-proxy",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Transparent stdio proxy that records MCP tool call interactions for the Lowdown reputation experiment.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"lowdown-proxy": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/cli.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"dev": "tsx src/cli.ts"
|
|
16
|
+
},
|
|
17
|
+
"keywords": ["mcp", "agent", "reputation", "interaction", "lowdown"],
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@supabase/supabase-js": "^2.45.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^22.0.0",
|
|
24
|
+
"tsx": "^4.16.0",
|
|
25
|
+
"typescript": "^5.5.0"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18"
|
|
29
|
+
}
|
|
30
|
+
}
|