@obov/mext 0.0.0-stage → 0.2.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/dist/build-cli.d.ts +39 -0
- package/dist/build-cli.js +167 -0
- package/dist/build-cli.js.map +1 -0
- package/dist/build.d.ts +65 -0
- package/dist/build.js +252 -0
- package/dist/build.js.map +1 -0
- package/dist/cli/args.d.ts +107 -0
- package/dist/cli/args.js +621 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/bin.d.ts +2 -0
- package/dist/cli/bin.js +13 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/build.d.ts +11 -0
- package/dist/cli/build.js +87 -0
- package/dist/cli/build.js.map +1 -0
- package/dist/cli/dev-lock.d.ts +23 -0
- package/dist/cli/dev-lock.js +68 -0
- package/dist/cli/dev-lock.js.map +1 -0
- package/dist/cli/dev-server.d.ts +20 -0
- package/dist/cli/dev-server.js +75 -0
- package/dist/cli/dev-server.js.map +1 -0
- package/dist/cli/dev.d.ts +21 -0
- package/dist/cli/dev.js +565 -0
- package/dist/cli/dev.js.map +1 -0
- package/dist/cli/help.d.ts +31 -0
- package/dist/cli/help.js +238 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/io.d.ts +26 -0
- package/dist/cli/io.js +74 -0
- package/dist/cli/io.js.map +1 -0
- package/dist/cli/login.d.ts +32 -0
- package/dist/cli/login.js +284 -0
- package/dist/cli/login.js.map +1 -0
- package/dist/cli/logout.d.ts +9 -0
- package/dist/cli/logout.js +105 -0
- package/dist/cli/logout.js.map +1 -0
- package/dist/cli/main.d.ts +19 -0
- package/dist/cli/main.js +77 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/mcp.d.ts +16 -0
- package/dist/cli/mcp.js +92 -0
- package/dist/cli/mcp.js.map +1 -0
- package/dist/cli/output.d.ts +78 -0
- package/dist/cli/output.js +229 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/remote.d.ts +17 -0
- package/dist/cli/remote.js +35 -0
- package/dist/cli/remote.js.map +1 -0
- package/dist/cli/run.d.ts +14 -0
- package/dist/cli/run.js +218 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/start.d.ts +8 -0
- package/dist/cli/start.js +37 -0
- package/dist/cli/start.js.map +1 -0
- package/dist/client/cache.d.ts +104 -0
- package/dist/client/cache.js +208 -0
- package/dist/client/cache.js.map +1 -0
- package/dist/client/main.d.ts +31 -0
- package/dist/client/main.js +101 -0
- package/dist/client/main.js.map +1 -0
- package/dist/client/manifest.d.ts +19 -0
- package/dist/client/manifest.js +68 -0
- package/dist/client/manifest.js.map +1 -0
- package/dist/client/runner.d.ts +52 -0
- package/dist/client/runner.js +563 -0
- package/dist/client/runner.js.map +1 -0
- package/dist/command.d.ts +75 -0
- package/dist/command.js +44 -0
- package/dist/command.js.map +1 -0
- package/dist/config.d.ts +111 -0
- package/dist/config.js +197 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.d.ts +74 -0
- package/dist/errors.js +38 -0
- package/dist/errors.js.map +1 -0
- package/dist/ids.d.ts +6 -0
- package/dist/ids.js +12 -0
- package/dist/ids.js.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/instrumentation.d.ts +97 -0
- package/dist/instrumentation.js +165 -0
- package/dist/instrumentation.js.map +1 -0
- package/dist/invoke.d.ts +64 -0
- package/dist/invoke.js +152 -0
- package/dist/invoke.js.map +1 -0
- package/dist/lookup.d.ts +9 -0
- package/dist/lookup.js +27 -0
- package/dist/lookup.js.map +1 -0
- package/dist/manifest.d.ts +68 -0
- package/dist/manifest.js +77 -0
- package/dist/manifest.js.map +1 -0
- package/dist/mcp/constants.d.ts +9 -0
- package/dist/mcp/constants.js +10 -0
- package/dist/mcp/constants.js.map +1 -0
- package/dist/mcp/dispatch.d.ts +66 -0
- package/dist/mcp/dispatch.js +249 -0
- package/dist/mcp/dispatch.js.map +1 -0
- package/dist/mcp/help.d.ts +57 -0
- package/dist/mcp/help.js +136 -0
- package/dist/mcp/help.js.map +1 -0
- package/dist/mcp/http.d.ts +96 -0
- package/dist/mcp/http.js +368 -0
- package/dist/mcp/http.js.map +1 -0
- package/dist/mcp/index.d.ts +15 -0
- package/dist/mcp/index.js +9 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/install.d.ts +17 -0
- package/dist/mcp/install.js +36 -0
- package/dist/mcp/install.js.map +1 -0
- package/dist/mcp/mrtr.d.ts +82 -0
- package/dist/mcp/mrtr.js +130 -0
- package/dist/mcp/mrtr.js.map +1 -0
- package/dist/mcp/server.d.ts +63 -0
- package/dist/mcp/server.js +210 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/stdio.d.ts +75 -0
- package/dist/mcp/stdio.js +222 -0
- package/dist/mcp/stdio.js.map +1 -0
- package/dist/principal.d.ts +16 -0
- package/dist/principal.js +24 -0
- package/dist/principal.js.map +1 -0
- package/dist/registry.d.ts +90 -0
- package/dist/registry.js +169 -0
- package/dist/registry.js.map +1 -0
- package/dist/remote/browser.d.ts +15 -0
- package/dist/remote/browser.js +47 -0
- package/dist/remote/browser.js.map +1 -0
- package/dist/remote/client.d.ts +113 -0
- package/dist/remote/client.js +312 -0
- package/dist/remote/client.js.map +1 -0
- package/dist/remote/credentials.d.ts +48 -0
- package/dist/remote/credentials.js +196 -0
- package/dist/remote/credentials.js.map +1 -0
- package/dist/remote/index.d.ts +8 -0
- package/dist/remote/index.js +5 -0
- package/dist/remote/index.js.map +1 -0
- package/dist/remote/oauth.d.ts +186 -0
- package/dist/remote/oauth.js +507 -0
- package/dist/remote/oauth.js.map +1 -0
- package/dist/scan.d.ts +34 -0
- package/dist/scan.js +155 -0
- package/dist/scan.js.map +1 -0
- package/dist/start.d.ts +63 -0
- package/dist/start.js +269 -0
- package/dist/start.js.map +1 -0
- package/dist/suggest.d.ts +7 -0
- package/dist/suggest.js +45 -0
- package/dist/suggest.js.map +1 -0
- package/dist/version.d.ts +4 -0
- package/dist/version.js +13 -0
- package/dist/version.js.map +1 -0
- package/package.json +49 -4
- package/README.md +0 -3
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
export function browserDecision(env, platform = process.platform) {
|
|
3
|
+
if (["SSH_CONNECTION", "SSH_CLIENT", "SSH_TTY"].some((k) => nonEmpty(env[k])))
|
|
4
|
+
return { open: false, reason: "ssh" };
|
|
5
|
+
if (platform === "linux" && !nonEmpty(env.DISPLAY) && !nonEmpty(env.WAYLAND_DISPLAY)) {
|
|
6
|
+
return { open: false, reason: "no-display" };
|
|
7
|
+
}
|
|
8
|
+
return { open: true };
|
|
9
|
+
}
|
|
10
|
+
/** 기본 opener. BROWSER = 명령 (공백 구분 인자, %s 가 있으면 URL 로 치환 · 없으면 끝에 URL) */
|
|
11
|
+
export function systemBrowserOpener(env, platform = process.platform) {
|
|
12
|
+
return (url) => {
|
|
13
|
+
let command;
|
|
14
|
+
let args;
|
|
15
|
+
const browser = env.BROWSER?.trim();
|
|
16
|
+
if (browser) {
|
|
17
|
+
const parts = browser.split(/\s+/);
|
|
18
|
+
command = parts[0];
|
|
19
|
+
const rest = parts.slice(1);
|
|
20
|
+
args = rest.some((p) => p.includes("%s")) ? rest.map((p) => p.replace(/%s/g, url)) : [...rest, url];
|
|
21
|
+
}
|
|
22
|
+
else if (platform === "darwin") {
|
|
23
|
+
command = "open";
|
|
24
|
+
args = [url];
|
|
25
|
+
}
|
|
26
|
+
else if (platform === "win32") {
|
|
27
|
+
command = "cmd";
|
|
28
|
+
args = ["/c", "start", '""', url.replace(/&/g, "^&")];
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
command = "xdg-open";
|
|
32
|
+
args = [url];
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
36
|
+
child.on("error", () => { });
|
|
37
|
+
child.unref();
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// 브라우저 없음
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function nonEmpty(value) {
|
|
45
|
+
return value !== undefined && value !== "";
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.js","sourceRoot":"","sources":["../../src/remote/browser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAA;AAiB1C,MAAM,UAAU,eAAe,CAC7B,GAAuC,EACvC,QAAQ,GAAoB,OAAO,CAAC,QAAQ;IAE5C,IAAI,CAAC,gBAAgB,EAAE,YAAY,EAAE,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAA;IACpH,IAAI,QAAQ,KAAK,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,eAAe,CAAC,EAAE,CAAC;QACrF,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,CAAA;IAC9C,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAA;AACvB,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,mBAAmB,CACjC,GAAuC,EACvC,QAAQ,GAAoB,OAAO,CAAC,QAAQ;IAE5C,OAAO,CAAC,GAAG,EAAE,EAAE;QACb,IAAI,OAAe,CAAA;QACnB,IAAI,IAAc,CAAA;QAClB,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,EAAE,IAAI,EAAE,CAAA;QACnC,IAAI,OAAO,EAAE,CAAC;YACZ,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;YAClC,OAAO,GAAG,KAAK,CAAC,CAAC,CAAE,CAAA;YACnB,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;YAC3B,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,EAAE,GAAG,CAAC,CAAA;QACrG,CAAC;aAAM,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACjC,OAAO,GAAG,MAAM,CAAA;YAChB,IAAI,GAAG,CAAC,GAAG,CAAC,CAAA;QACd,CAAC;aAAM,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;YAChC,OAAO,GAAG,KAAK,CAAA;YACf,IAAI,GAAG,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAA;QACvD,CAAC;aAAM,CAAC;YACN,OAAO,GAAG,UAAU,CAAA;YACpB,IAAI,GAAG,CAAC,GAAG,CAAC,CAAA;QACd,CAAC;QACD,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAA;YACvE,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;YAC3B,KAAK,CAAC,KAAK,EAAE,CAAA;QACf,CAAC;QAAC,MAAM,CAAC;YACP,UAAU;QACZ,CAAC;IACH,CAAC,CAAA;AACH,CAAC;AAED,SAAS,QAAQ,CAAC,KAAyB;IACzC,OAAO,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE,CAAA;AAC5C,CAAC","sourcesContent":["import { spawn } from \"node:child_process\"\n\n/**\n * login 의 브라우저 판정 · 열기 (spec §5 login 브라우저 판정 · headless).\n * 열지 않음 = SSH 세션 (SSH_CONNECTION · SSH_CLIENT · SSH_TTY) · Linux 에서 DISPLAY · WAYLAND_DISPLAY 없음.\n * 여는 명령 = BROWSER (있으면) → open (macOS) · start (Windows) · xdg-open.\n */\n\nexport interface BrowserDecision {\n open: boolean\n /** 열지 않는 이유 (안내 출력용) */\n reason?: \"ssh\" | \"no-display\"\n}\n\n/** URL 을 브라우저로 연다. 실패는 무시 (URL 은 항상 stderr 에 출력) */\nexport type BrowserOpener = (url: string) => void\n\nexport function browserDecision(\n env: Record<string, string | undefined>,\n platform: NodeJS.Platform = process.platform,\n): BrowserDecision {\n if ([\"SSH_CONNECTION\", \"SSH_CLIENT\", \"SSH_TTY\"].some((k) => nonEmpty(env[k]))) return { open: false, reason: \"ssh\" }\n if (platform === \"linux\" && !nonEmpty(env.DISPLAY) && !nonEmpty(env.WAYLAND_DISPLAY)) {\n return { open: false, reason: \"no-display\" }\n }\n return { open: true }\n}\n\n/** 기본 opener. BROWSER = 명령 (공백 구분 인자, %s 가 있으면 URL 로 치환 · 없으면 끝에 URL) */\nexport function systemBrowserOpener(\n env: Record<string, string | undefined>,\n platform: NodeJS.Platform = process.platform,\n): BrowserOpener {\n return (url) => {\n let command: string\n let args: string[]\n const browser = env.BROWSER?.trim()\n if (browser) {\n const parts = browser.split(/\\s+/)\n command = parts[0]!\n const rest = parts.slice(1)\n args = rest.some((p) => p.includes(\"%s\")) ? rest.map((p) => p.replace(/%s/g, url)) : [...rest, url]\n } else if (platform === \"darwin\") {\n command = \"open\"\n args = [url]\n } else if (platform === \"win32\") {\n command = \"cmd\"\n args = [\"/c\", \"start\", '\"\"', url.replace(/&/g, \"^&\")]\n } else {\n command = \"xdg-open\"\n args = [url]\n }\n try {\n const child = spawn(command, args, { detached: true, stdio: \"ignore\" })\n child.on(\"error\", () => {})\n child.unref()\n } catch {\n // 브라우저 없음\n }\n }\n}\n\nfunction nonEmpty(value: string | undefined): boolean {\n return value !== undefined && value !== \"\"\n}\n"]}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 원격 실행용 최소 MCP 클라이언트 (Streamable HTTP, 프로토콜 2026-07-28, stateless).
|
|
3
|
+
* 요청마다 POST 1회: _meta envelope (버전 · clientInfo · clientCapabilities) + MCP-Protocol-Version · Mcp-Method ·
|
|
4
|
+
* Mcp-Name · Mcp-Param-* 헤더. 응답 = application/json 또는 text/event-stream (progress 알림 → onNotification).
|
|
5
|
+
* MRTR (InputRequiredResult) 재시도 루프는 callTool 의 onInputRequired 가 담당.
|
|
6
|
+
*/
|
|
7
|
+
export declare const PROTOCOL_VERSION = "2026-07-28";
|
|
8
|
+
export declare const META_KEYS: {
|
|
9
|
+
readonly protocolVersion: "io.modelcontextprotocol/protocolVersion";
|
|
10
|
+
readonly clientInfo: "io.modelcontextprotocol/clientInfo";
|
|
11
|
+
readonly clientCapabilities: "io.modelcontextprotocol/clientCapabilities";
|
|
12
|
+
};
|
|
13
|
+
/** 클라이언트가 처리하는 입력 요청 = elicitation form 만 (sampling · roots 없음) */
|
|
14
|
+
export declare const CLIENT_CAPABILITIES: {
|
|
15
|
+
readonly elicitation: {
|
|
16
|
+
readonly form: {};
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
/** MRTR 재시도 상한 (같은 요청의 input_required 연속 횟수) */
|
|
20
|
+
export declare const MAX_INPUT_ROUNDS = 8;
|
|
21
|
+
export interface RemoteTool {
|
|
22
|
+
name: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
inputSchema?: Record<string, unknown>;
|
|
25
|
+
annotations?: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
export interface CallToolResult {
|
|
28
|
+
content?: unknown[];
|
|
29
|
+
structuredContent?: unknown;
|
|
30
|
+
isError?: boolean;
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
}
|
|
33
|
+
/** InputRequiredResult (resultType: input_required) */
|
|
34
|
+
export interface InputRequired {
|
|
35
|
+
inputRequests?: Record<string, InputRequestObject>;
|
|
36
|
+
requestState?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface InputRequestObject {
|
|
39
|
+
method: string;
|
|
40
|
+
params?: Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
/** 입력 요청 key → 응답 (ElicitResult 등). 이번 라운드 요청에 대한 것만 */
|
|
43
|
+
export type InputResponses = Record<string, unknown>;
|
|
44
|
+
export interface RequestOptions {
|
|
45
|
+
signal?: AbortSignal;
|
|
46
|
+
/** 서버 → 클라이언트 알림 (notifications/progress 등) */
|
|
47
|
+
onNotification?: (method: string, params: Record<string, unknown>) => void;
|
|
48
|
+
/** _meta.progressToken. 있으면 서버가 SSE 로 응답 */
|
|
49
|
+
progressToken?: string | number;
|
|
50
|
+
}
|
|
51
|
+
export interface CallToolOptions extends RequestOptions {
|
|
52
|
+
/** input_required 응답 처리 → 이번 라운드의 inputResponses. 없으면 RemoteInputRequiredError */
|
|
53
|
+
onInputRequired?: (request: InputRequired, round: number) => Promise<InputResponses>;
|
|
54
|
+
}
|
|
55
|
+
/** HTTP 401. challenge = WWW-Authenticate 파싱 결과, data = JSON-RPC error.data (mext 는 UNAUTHENTICATED toJSON) */
|
|
56
|
+
export declare class RemoteUnauthorizedError extends Error {
|
|
57
|
+
readonly challenge: BearerChallenge;
|
|
58
|
+
readonly data: unknown;
|
|
59
|
+
constructor(message: string, challenge: BearerChallenge, data: unknown);
|
|
60
|
+
}
|
|
61
|
+
/** JSON-RPC 오류 응답 (HTTP 400 · 403 · 500 포함) */
|
|
62
|
+
export declare class RemoteRpcError extends Error {
|
|
63
|
+
readonly code: number;
|
|
64
|
+
readonly data: unknown;
|
|
65
|
+
readonly status: number;
|
|
66
|
+
constructor(message: string, code: number, data: unknown, status: number);
|
|
67
|
+
}
|
|
68
|
+
/** 연결 실패 · JSON-RPC 가 아닌 응답 */
|
|
69
|
+
export declare class RemoteTransportError extends Error {
|
|
70
|
+
constructor(message: string, options?: {
|
|
71
|
+
cause?: unknown;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/** onInputRequired 없이 input_required 를 받음 · 라운드 상한 초과 */
|
|
75
|
+
export declare class RemoteInputRequiredError extends Error {
|
|
76
|
+
readonly request: InputRequired;
|
|
77
|
+
constructor(message: string, request: InputRequired);
|
|
78
|
+
}
|
|
79
|
+
export interface RemoteClientOptions {
|
|
80
|
+
/** MCP 엔드포인트 (http(s)://host/mcp) */
|
|
81
|
+
url: string;
|
|
82
|
+
/** 요청마다 호출. 없으면 Authorization 헤더 없음 */
|
|
83
|
+
token?: () => string | undefined | Promise<string | undefined>;
|
|
84
|
+
fetch?: typeof fetch;
|
|
85
|
+
clientInfo?: {
|
|
86
|
+
name: string;
|
|
87
|
+
version: string;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/** tools/list 전체 페이지 + 첫 페이지의 캐시 힌트 (2026-07-28 ttlMs · cacheScope) */
|
|
91
|
+
export interface ToolsListResult {
|
|
92
|
+
tools: RemoteTool[];
|
|
93
|
+
ttlMs?: number;
|
|
94
|
+
cacheScope?: string;
|
|
95
|
+
}
|
|
96
|
+
export interface RemoteClient {
|
|
97
|
+
readonly url: string;
|
|
98
|
+
request(method: string, params?: Record<string, unknown>, options?: RequestOptions): Promise<Record<string, unknown>>;
|
|
99
|
+
listTools(options?: RequestOptions): Promise<RemoteTool[]>;
|
|
100
|
+
listToolsResult(options?: RequestOptions): Promise<ToolsListResult>;
|
|
101
|
+
callTool(tool: RemoteTool, args: Record<string, unknown>, options?: CallToolOptions): Promise<CallToolResult>;
|
|
102
|
+
}
|
|
103
|
+
export declare function createRemoteClient(options: RemoteClientOptions): RemoteClient;
|
|
104
|
+
export interface BearerChallenge {
|
|
105
|
+
/** Bearer challenge 가 있었는지 */
|
|
106
|
+
bearer: boolean;
|
|
107
|
+
error?: string;
|
|
108
|
+
scope?: string;
|
|
109
|
+
resourceMetadata?: string;
|
|
110
|
+
}
|
|
111
|
+
/** WWW-Authenticate 의 Bearer challenge 파라미터 (RFC 6750 · RFC 9728 resource_metadata) */
|
|
112
|
+
export declare function parseBearerChallenge(header: string | null): BearerChallenge;
|
|
113
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { VERSION } from "../version.js";
|
|
2
|
+
/**
|
|
3
|
+
* 원격 실행용 최소 MCP 클라이언트 (Streamable HTTP, 프로토콜 2026-07-28, stateless).
|
|
4
|
+
* 요청마다 POST 1회: _meta envelope (버전 · clientInfo · clientCapabilities) + MCP-Protocol-Version · Mcp-Method ·
|
|
5
|
+
* Mcp-Name · Mcp-Param-* 헤더. 응답 = application/json 또는 text/event-stream (progress 알림 → onNotification).
|
|
6
|
+
* MRTR (InputRequiredResult) 재시도 루프는 callTool 의 onInputRequired 가 담당.
|
|
7
|
+
*/
|
|
8
|
+
export const PROTOCOL_VERSION = "2026-07-28";
|
|
9
|
+
export const META_KEYS = {
|
|
10
|
+
protocolVersion: "io.modelcontextprotocol/protocolVersion",
|
|
11
|
+
clientInfo: "io.modelcontextprotocol/clientInfo",
|
|
12
|
+
clientCapabilities: "io.modelcontextprotocol/clientCapabilities",
|
|
13
|
+
};
|
|
14
|
+
/** 클라이언트가 처리하는 입력 요청 = elicitation form 만 (sampling · roots 없음) */
|
|
15
|
+
export const CLIENT_CAPABILITIES = { elicitation: { form: {} } };
|
|
16
|
+
/** MRTR 재시도 상한 (같은 요청의 input_required 연속 횟수) */
|
|
17
|
+
export const MAX_INPUT_ROUNDS = 8;
|
|
18
|
+
/** HTTP 401. challenge = WWW-Authenticate 파싱 결과, data = JSON-RPC error.data (mext 는 UNAUTHENTICATED toJSON) */
|
|
19
|
+
export class RemoteUnauthorizedError extends Error {
|
|
20
|
+
challenge;
|
|
21
|
+
data;
|
|
22
|
+
constructor(message, challenge, data) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "RemoteUnauthorizedError";
|
|
25
|
+
this.challenge = challenge;
|
|
26
|
+
this.data = data;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** JSON-RPC 오류 응답 (HTTP 400 · 403 · 500 포함) */
|
|
30
|
+
export class RemoteRpcError extends Error {
|
|
31
|
+
code;
|
|
32
|
+
data;
|
|
33
|
+
status;
|
|
34
|
+
constructor(message, code, data, status) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = "RemoteRpcError";
|
|
37
|
+
this.code = code;
|
|
38
|
+
this.data = data;
|
|
39
|
+
this.status = status;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** 연결 실패 · JSON-RPC 가 아닌 응답 */
|
|
43
|
+
export class RemoteTransportError extends Error {
|
|
44
|
+
constructor(message, options) {
|
|
45
|
+
super(message, options);
|
|
46
|
+
this.name = "RemoteTransportError";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** onInputRequired 없이 input_required 를 받음 · 라운드 상한 초과 */
|
|
50
|
+
export class RemoteInputRequiredError extends Error {
|
|
51
|
+
request;
|
|
52
|
+
constructor(message, request) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.name = "RemoteInputRequiredError";
|
|
55
|
+
this.request = request;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function createRemoteClient(options) {
|
|
59
|
+
const doFetch = options.fetch ?? fetch;
|
|
60
|
+
const clientInfo = options.clientInfo ?? { name: "mext", version: VERSION };
|
|
61
|
+
let nextId = 1;
|
|
62
|
+
const request = async (method, params = {}, opts = {}, extraHeaders = {}) => {
|
|
63
|
+
const id = nextId++;
|
|
64
|
+
const meta = {
|
|
65
|
+
...(isRecord(params._meta) ? params._meta : {}),
|
|
66
|
+
[META_KEYS.protocolVersion]: PROTOCOL_VERSION,
|
|
67
|
+
[META_KEYS.clientInfo]: clientInfo,
|
|
68
|
+
[META_KEYS.clientCapabilities]: CLIENT_CAPABILITIES,
|
|
69
|
+
};
|
|
70
|
+
if (opts.progressToken !== undefined)
|
|
71
|
+
meta.progressToken = opts.progressToken;
|
|
72
|
+
const body = { jsonrpc: "2.0", id, method, params: { ...params, _meta: meta } };
|
|
73
|
+
const headers = {
|
|
74
|
+
"Content-Type": "application/json",
|
|
75
|
+
Accept: "application/json, text/event-stream",
|
|
76
|
+
"MCP-Protocol-Version": PROTOCOL_VERSION,
|
|
77
|
+
"Mcp-Method": method,
|
|
78
|
+
...extraHeaders,
|
|
79
|
+
};
|
|
80
|
+
const token = await options.token?.();
|
|
81
|
+
if (token)
|
|
82
|
+
headers.Authorization = `Bearer ${token}`;
|
|
83
|
+
let res;
|
|
84
|
+
try {
|
|
85
|
+
res = await doFetch(options.url, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers,
|
|
88
|
+
body: JSON.stringify(body),
|
|
89
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (opts.signal?.aborted)
|
|
94
|
+
throw error;
|
|
95
|
+
throw new RemoteTransportError(`Cannot reach ${options.url}: ${errorMessage(error)}`, { cause: error });
|
|
96
|
+
}
|
|
97
|
+
const type = res.headers.get("content-type") ?? "";
|
|
98
|
+
if (res.status === 401) {
|
|
99
|
+
const payload = await readJson(res);
|
|
100
|
+
const err = isRecord(payload) && isRecord(payload.error) ? payload.error : {};
|
|
101
|
+
throw new RemoteUnauthorizedError(typeof err.message === "string" ? err.message : "Authentication required.", parseBearerChallenge(res.headers.get("www-authenticate")), err.data);
|
|
102
|
+
}
|
|
103
|
+
if (type.includes("text/event-stream")) {
|
|
104
|
+
return handleMessage(await readSse(res, id, opts), id, res.status);
|
|
105
|
+
}
|
|
106
|
+
const payload = await readJson(res);
|
|
107
|
+
if (payload === undefined) {
|
|
108
|
+
throw new RemoteTransportError(`${options.url} returned HTTP ${res.status} without a JSON-RPC response.`);
|
|
109
|
+
}
|
|
110
|
+
return handleMessage(payload, id, res.status);
|
|
111
|
+
};
|
|
112
|
+
const handleMessage = (message, id, status) => {
|
|
113
|
+
if (!isRecord(message))
|
|
114
|
+
throw new RemoteTransportError(`Invalid JSON-RPC response (HTTP ${status}).`);
|
|
115
|
+
if (isRecord(message.error)) {
|
|
116
|
+
const e = message.error;
|
|
117
|
+
throw new RemoteRpcError(typeof e.message === "string" ? e.message : "JSON-RPC error", typeof e.code === "number" ? e.code : -32_603, e.data, status);
|
|
118
|
+
}
|
|
119
|
+
if (message.id !== id || !isRecord(message.result)) {
|
|
120
|
+
throw new RemoteTransportError(`Invalid JSON-RPC response (HTTP ${status}).`);
|
|
121
|
+
}
|
|
122
|
+
return message.result;
|
|
123
|
+
};
|
|
124
|
+
const listToolsResult = async (opts = {}) => {
|
|
125
|
+
const tools = [];
|
|
126
|
+
const out = { tools };
|
|
127
|
+
let cursor;
|
|
128
|
+
for (let page = 0; page < 100; page++) {
|
|
129
|
+
const result = await request("tools/list", cursor === undefined ? {} : { cursor }, opts);
|
|
130
|
+
if (page === 0) {
|
|
131
|
+
if (typeof result.ttlMs === "number" && Number.isFinite(result.ttlMs) && result.ttlMs >= 0)
|
|
132
|
+
out.ttlMs = result.ttlMs;
|
|
133
|
+
if (typeof result.cacheScope === "string")
|
|
134
|
+
out.cacheScope = result.cacheScope;
|
|
135
|
+
}
|
|
136
|
+
if (Array.isArray(result.tools)) {
|
|
137
|
+
for (const t of result.tools)
|
|
138
|
+
if (isRecord(t) && typeof t.name === "string")
|
|
139
|
+
tools.push(t);
|
|
140
|
+
}
|
|
141
|
+
if (typeof result.nextCursor !== "string" || result.nextCursor === "")
|
|
142
|
+
break;
|
|
143
|
+
cursor = result.nextCursor;
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
};
|
|
147
|
+
const callTool = async (tool, args, opts = {}) => {
|
|
148
|
+
const headers = { "Mcp-Name": tool.name, ...paramHeaders(tool, args) };
|
|
149
|
+
let retry = {};
|
|
150
|
+
for (let round = 0;; round++) {
|
|
151
|
+
const result = await request("tools/call", { name: tool.name, arguments: args, ...retry }, opts, headers);
|
|
152
|
+
if (result.resultType !== "input_required")
|
|
153
|
+
return result;
|
|
154
|
+
const required = {};
|
|
155
|
+
if (isRecord(result.inputRequests))
|
|
156
|
+
required.inputRequests = result.inputRequests;
|
|
157
|
+
if (typeof result.requestState === "string")
|
|
158
|
+
required.requestState = result.requestState;
|
|
159
|
+
if (!opts.onInputRequired)
|
|
160
|
+
throw new RemoteInputRequiredError(`${tool.name} requires input.`, required);
|
|
161
|
+
if (round >= MAX_INPUT_ROUNDS) {
|
|
162
|
+
throw new RemoteInputRequiredError(`${tool.name} still required input after ${MAX_INPUT_ROUNDS} rounds.`, required);
|
|
163
|
+
}
|
|
164
|
+
// 요청이 있으면 응답을 만든 뒤 재시도. requestState 는 그대로 되돌려 보내고, 없으면 보내지 않는다
|
|
165
|
+
const responses = required.inputRequests ? await opts.onInputRequired(required, round) : undefined;
|
|
166
|
+
retry = {};
|
|
167
|
+
if (responses !== undefined)
|
|
168
|
+
retry.inputResponses = responses;
|
|
169
|
+
if (required.requestState !== undefined)
|
|
170
|
+
retry.requestState = required.requestState;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
return {
|
|
174
|
+
url: options.url,
|
|
175
|
+
request: (method, params, opts) => request(method, params, opts),
|
|
176
|
+
listTools: async (opts) => (await listToolsResult(opts)).tools,
|
|
177
|
+
listToolsResult,
|
|
178
|
+
callTool,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/** inputSchema 의 x-mcp-header 속성 → Mcp-Param-<Name> 헤더 (SEP-2243). 문자열 · 숫자 · boolean 값만 */
|
|
182
|
+
function paramHeaders(tool, args) {
|
|
183
|
+
const props = tool.inputSchema?.properties;
|
|
184
|
+
if (!isRecord(props))
|
|
185
|
+
return {};
|
|
186
|
+
const out = {};
|
|
187
|
+
for (const [key, schema] of Object.entries(props)) {
|
|
188
|
+
if (!isRecord(schema) || typeof schema["x-mcp-header"] !== "string")
|
|
189
|
+
continue;
|
|
190
|
+
const value = args[key];
|
|
191
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
192
|
+
out[`Mcp-Param-${schema["x-mcp-header"]}`] = String(value);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
/* SSE */
|
|
198
|
+
/** 응답 스트림에서 id 의 응답 메시지가 올 때까지 읽는다. 알림은 콜백, 응답 후 스트림을 닫는다 */
|
|
199
|
+
async function readSse(res, id, opts) {
|
|
200
|
+
if (!res.body)
|
|
201
|
+
throw new RemoteTransportError("Empty event stream.");
|
|
202
|
+
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
|
|
203
|
+
let buffer = "";
|
|
204
|
+
let data = [];
|
|
205
|
+
const dispatch = () => {
|
|
206
|
+
if (data.length === 0)
|
|
207
|
+
return undefined;
|
|
208
|
+
const text = data.join("\n");
|
|
209
|
+
data = [];
|
|
210
|
+
let message;
|
|
211
|
+
try {
|
|
212
|
+
message = JSON.parse(text);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
if (!isRecord(message))
|
|
218
|
+
return undefined;
|
|
219
|
+
if (typeof message.method === "string" && message.id === undefined) {
|
|
220
|
+
try {
|
|
221
|
+
opts.onNotification?.(message.method, isRecord(message.params) ? message.params : {});
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
// 표시 오류는 응답에 영향 없음
|
|
225
|
+
}
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
if (message.id === id && ("result" in message || "error" in message))
|
|
229
|
+
return message;
|
|
230
|
+
return undefined;
|
|
231
|
+
};
|
|
232
|
+
try {
|
|
233
|
+
for (;;) {
|
|
234
|
+
const { done, value } = await reader.read();
|
|
235
|
+
if (done)
|
|
236
|
+
break;
|
|
237
|
+
buffer += value;
|
|
238
|
+
let nl;
|
|
239
|
+
while ((nl = buffer.search(/\r\n|\r|\n/)) >= 0) {
|
|
240
|
+
const line = buffer.slice(0, nl);
|
|
241
|
+
buffer = buffer.slice(nl + (buffer.startsWith("\r\n", nl) ? 2 : 1));
|
|
242
|
+
if (line === "") {
|
|
243
|
+
const found = dispatch();
|
|
244
|
+
if (found !== undefined)
|
|
245
|
+
return found;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (line.startsWith(":"))
|
|
249
|
+
continue;
|
|
250
|
+
const colon = line.indexOf(":");
|
|
251
|
+
const field = colon < 0 ? line : line.slice(0, colon);
|
|
252
|
+
let value = colon < 0 ? "" : line.slice(colon + 1);
|
|
253
|
+
if (value.startsWith(" "))
|
|
254
|
+
value = value.slice(1);
|
|
255
|
+
if (field === "data")
|
|
256
|
+
data.push(value);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const found = dispatch();
|
|
260
|
+
if (found !== undefined)
|
|
261
|
+
return found;
|
|
262
|
+
}
|
|
263
|
+
finally {
|
|
264
|
+
await reader.cancel().catch(() => { });
|
|
265
|
+
}
|
|
266
|
+
if (opts.signal?.aborted)
|
|
267
|
+
throw opts.signal.reason;
|
|
268
|
+
throw new RemoteTransportError("Event stream ended without a response.");
|
|
269
|
+
}
|
|
270
|
+
async function readJson(res) {
|
|
271
|
+
const text = await res.text().catch(() => "");
|
|
272
|
+
if (text === "")
|
|
273
|
+
return undefined;
|
|
274
|
+
try {
|
|
275
|
+
return JSON.parse(text);
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/** WWW-Authenticate 의 Bearer challenge 파라미터 (RFC 6750 · RFC 9728 resource_metadata) */
|
|
282
|
+
export function parseBearerChallenge(header) {
|
|
283
|
+
if (!header)
|
|
284
|
+
return { bearer: false };
|
|
285
|
+
const m = /(?:^|,\s*)Bearer\b(.*)$/i.exec(header);
|
|
286
|
+
if (!m)
|
|
287
|
+
return { bearer: false };
|
|
288
|
+
const params = {};
|
|
289
|
+
const re = /([A-Za-z_][\w-]*)\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^\s,]+))/g;
|
|
290
|
+
for (let p = re.exec(m[1]); p !== null; p = re.exec(m[1])) {
|
|
291
|
+
params[p[1].toLowerCase()] = p[2] !== undefined ? p[2].replace(/\\(.)/g, "$1") : p[3];
|
|
292
|
+
}
|
|
293
|
+
const out = { bearer: true };
|
|
294
|
+
if (params.error !== undefined)
|
|
295
|
+
out.error = params.error;
|
|
296
|
+
if (params.scope !== undefined)
|
|
297
|
+
out.scope = params.scope;
|
|
298
|
+
if (params.resource_metadata !== undefined)
|
|
299
|
+
out.resourceMetadata = params.resource_metadata;
|
|
300
|
+
return out;
|
|
301
|
+
}
|
|
302
|
+
export function isRecord(value) {
|
|
303
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
304
|
+
}
|
|
305
|
+
function errorMessage(error) {
|
|
306
|
+
if (error instanceof Error) {
|
|
307
|
+
const cause = error.cause;
|
|
308
|
+
return cause instanceof Error && cause.message ? cause.message : error.message;
|
|
309
|
+
}
|
|
310
|
+
return String(error);
|
|
311
|
+
}
|
|
312
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/remote/client.ts"],"names":[],"mappings":"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","sourcesContent":["import { VERSION } from \"../version.ts\"\n\n/**\n * 원격 실행용 최소 MCP 클라이언트 (Streamable HTTP, 프로토콜 2026-07-28, stateless).\n * 요청마다 POST 1회: _meta envelope (버전 · clientInfo · clientCapabilities) + MCP-Protocol-Version · Mcp-Method ·\n * Mcp-Name · Mcp-Param-* 헤더. 응답 = application/json 또는 text/event-stream (progress 알림 → onNotification).\n * MRTR (InputRequiredResult) 재시도 루프는 callTool 의 onInputRequired 가 담당.\n */\n\nexport const PROTOCOL_VERSION = \"2026-07-28\"\n\nexport const META_KEYS = {\n protocolVersion: \"io.modelcontextprotocol/protocolVersion\",\n clientInfo: \"io.modelcontextprotocol/clientInfo\",\n clientCapabilities: \"io.modelcontextprotocol/clientCapabilities\",\n} as const\n\n/** 클라이언트가 처리하는 입력 요청 = elicitation form 만 (sampling · roots 없음) */\nexport const CLIENT_CAPABILITIES = { elicitation: { form: {} } } as const\n\n/** MRTR 재시도 상한 (같은 요청의 input_required 연속 횟수) */\nexport const MAX_INPUT_ROUNDS = 8\n\nexport interface RemoteTool {\n name: string\n description?: string\n inputSchema?: Record<string, unknown>\n annotations?: Record<string, unknown>\n}\n\nexport interface CallToolResult {\n content?: unknown[]\n structuredContent?: unknown\n isError?: boolean\n [key: string]: unknown\n}\n\n/** InputRequiredResult (resultType: input_required) */\nexport interface InputRequired {\n inputRequests?: Record<string, InputRequestObject>\n requestState?: string\n}\n\nexport interface InputRequestObject {\n method: string\n params?: Record<string, unknown>\n}\n\n/** 입력 요청 key → 응답 (ElicitResult 등). 이번 라운드 요청에 대한 것만 */\nexport type InputResponses = Record<string, unknown>\n\nexport interface RequestOptions {\n signal?: AbortSignal\n /** 서버 → 클라이언트 알림 (notifications/progress 등) */\n onNotification?: (method: string, params: Record<string, unknown>) => void\n /** _meta.progressToken. 있으면 서버가 SSE 로 응답 */\n progressToken?: string | number\n}\n\nexport interface CallToolOptions extends RequestOptions {\n /** input_required 응답 처리 → 이번 라운드의 inputResponses. 없으면 RemoteInputRequiredError */\n onInputRequired?: (request: InputRequired, round: number) => Promise<InputResponses>\n}\n\n/** HTTP 401. challenge = WWW-Authenticate 파싱 결과, data = JSON-RPC error.data (mext 는 UNAUTHENTICATED toJSON) */\nexport class RemoteUnauthorizedError extends Error {\n readonly challenge: BearerChallenge\n readonly data: unknown\n constructor(message: string, challenge: BearerChallenge, data: unknown) {\n super(message)\n this.name = \"RemoteUnauthorizedError\"\n this.challenge = challenge\n this.data = data\n }\n}\n\n/** JSON-RPC 오류 응답 (HTTP 400 · 403 · 500 포함) */\nexport class RemoteRpcError extends Error {\n readonly code: number\n readonly data: unknown\n readonly status: number\n constructor(message: string, code: number, data: unknown, status: number) {\n super(message)\n this.name = \"RemoteRpcError\"\n this.code = code\n this.data = data\n this.status = status\n }\n}\n\n/** 연결 실패 · JSON-RPC 가 아닌 응답 */\nexport class RemoteTransportError extends Error {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options)\n this.name = \"RemoteTransportError\"\n }\n}\n\n/** onInputRequired 없이 input_required 를 받음 · 라운드 상한 초과 */\nexport class RemoteInputRequiredError extends Error {\n readonly request: InputRequired\n constructor(message: string, request: InputRequired) {\n super(message)\n this.name = \"RemoteInputRequiredError\"\n this.request = request\n }\n}\n\nexport interface RemoteClientOptions {\n /** MCP 엔드포인트 (http(s)://host/mcp) */\n url: string\n /** 요청마다 호출. 없으면 Authorization 헤더 없음 */\n token?: () => string | undefined | Promise<string | undefined>\n fetch?: typeof fetch\n clientInfo?: { name: string; version: string }\n}\n\n/** tools/list 전체 페이지 + 첫 페이지의 캐시 힌트 (2026-07-28 ttlMs · cacheScope) */\nexport interface ToolsListResult {\n tools: RemoteTool[]\n ttlMs?: number\n cacheScope?: string\n}\n\nexport interface RemoteClient {\n readonly url: string\n request(method: string, params?: Record<string, unknown>, options?: RequestOptions): Promise<Record<string, unknown>>\n listTools(options?: RequestOptions): Promise<RemoteTool[]>\n listToolsResult(options?: RequestOptions): Promise<ToolsListResult>\n callTool(tool: RemoteTool, args: Record<string, unknown>, options?: CallToolOptions): Promise<CallToolResult>\n}\n\nexport function createRemoteClient(options: RemoteClientOptions): RemoteClient {\n const doFetch = options.fetch ?? fetch\n const clientInfo = options.clientInfo ?? { name: \"mext\", version: VERSION }\n let nextId = 1\n\n const request = async (\n method: string,\n params: Record<string, unknown> = {},\n opts: RequestOptions = {},\n extraHeaders: Record<string, string> = {},\n ): Promise<Record<string, unknown>> => {\n const id = nextId++\n const meta: Record<string, unknown> = {\n ...(isRecord(params._meta) ? params._meta : {}),\n [META_KEYS.protocolVersion]: PROTOCOL_VERSION,\n [META_KEYS.clientInfo]: clientInfo,\n [META_KEYS.clientCapabilities]: CLIENT_CAPABILITIES,\n }\n if (opts.progressToken !== undefined) meta.progressToken = opts.progressToken\n const body = { jsonrpc: \"2.0\", id, method, params: { ...params, _meta: meta } }\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json, text/event-stream\",\n \"MCP-Protocol-Version\": PROTOCOL_VERSION,\n \"Mcp-Method\": method,\n ...extraHeaders,\n }\n const token = await options.token?.()\n if (token) headers.Authorization = `Bearer ${token}`\n\n let res: Response\n try {\n res = await doFetch(options.url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(body),\n ...(opts.signal ? { signal: opts.signal } : {}),\n })\n } catch (error) {\n if (opts.signal?.aborted) throw error\n throw new RemoteTransportError(`Cannot reach ${options.url}: ${errorMessage(error)}`, { cause: error })\n }\n\n const type = res.headers.get(\"content-type\") ?? \"\"\n if (res.status === 401) {\n const payload = await readJson(res)\n const err = isRecord(payload) && isRecord(payload.error) ? payload.error : {}\n throw new RemoteUnauthorizedError(\n typeof err.message === \"string\" ? err.message : \"Authentication required.\",\n parseBearerChallenge(res.headers.get(\"www-authenticate\")),\n err.data,\n )\n }\n if (type.includes(\"text/event-stream\")) {\n return handleMessage(await readSse(res, id, opts), id, res.status)\n }\n const payload = await readJson(res)\n if (payload === undefined) {\n throw new RemoteTransportError(`${options.url} returned HTTP ${res.status} without a JSON-RPC response.`)\n }\n return handleMessage(payload, id, res.status)\n }\n\n const handleMessage = (message: unknown, id: number, status: number): Record<string, unknown> => {\n if (!isRecord(message)) throw new RemoteTransportError(`Invalid JSON-RPC response (HTTP ${status}).`)\n if (isRecord(message.error)) {\n const e = message.error\n throw new RemoteRpcError(\n typeof e.message === \"string\" ? e.message : \"JSON-RPC error\",\n typeof e.code === \"number\" ? e.code : -32_603,\n e.data,\n status,\n )\n }\n if (message.id !== id || !isRecord(message.result)) {\n throw new RemoteTransportError(`Invalid JSON-RPC response (HTTP ${status}).`)\n }\n return message.result\n }\n\n const listToolsResult = async (opts: RequestOptions = {}): Promise<ToolsListResult> => {\n const tools: RemoteTool[] = []\n const out: ToolsListResult = { tools }\n let cursor: string | undefined\n for (let page = 0; page < 100; page++) {\n const result = await request(\"tools/list\", cursor === undefined ? {} : { cursor }, opts)\n if (page === 0) {\n if (typeof result.ttlMs === \"number\" && Number.isFinite(result.ttlMs) && result.ttlMs >= 0) out.ttlMs = result.ttlMs\n if (typeof result.cacheScope === \"string\") out.cacheScope = result.cacheScope\n }\n if (Array.isArray(result.tools)) {\n for (const t of result.tools) if (isRecord(t) && typeof t.name === \"string\") tools.push(t as unknown as RemoteTool)\n }\n if (typeof result.nextCursor !== \"string\" || result.nextCursor === \"\") break\n cursor = result.nextCursor\n }\n return out\n }\n\n const callTool = async (\n tool: RemoteTool,\n args: Record<string, unknown>,\n opts: CallToolOptions = {},\n ): Promise<CallToolResult> => {\n const headers: Record<string, string> = { \"Mcp-Name\": tool.name, ...paramHeaders(tool, args) }\n let retry: { inputResponses?: InputResponses; requestState?: string } = {}\n for (let round = 0; ; round++) {\n const result = await request(\"tools/call\", { name: tool.name, arguments: args, ...retry }, opts, headers)\n if (result.resultType !== \"input_required\") return result as CallToolResult\n const required: InputRequired = {}\n if (isRecord(result.inputRequests)) required.inputRequests = result.inputRequests as InputRequired[\"inputRequests\"]\n if (typeof result.requestState === \"string\") required.requestState = result.requestState\n if (!opts.onInputRequired) throw new RemoteInputRequiredError(`${tool.name} requires input.`, required)\n if (round >= MAX_INPUT_ROUNDS) {\n throw new RemoteInputRequiredError(`${tool.name} still required input after ${MAX_INPUT_ROUNDS} rounds.`, required)\n }\n // 요청이 있으면 응답을 만든 뒤 재시도. requestState 는 그대로 되돌려 보내고, 없으면 보내지 않는다\n const responses = required.inputRequests ? await opts.onInputRequired(required, round) : undefined\n retry = {}\n if (responses !== undefined) retry.inputResponses = responses\n if (required.requestState !== undefined) retry.requestState = required.requestState\n }\n }\n\n return {\n url: options.url,\n request: (method, params, opts) => request(method, params, opts),\n listTools: async (opts) => (await listToolsResult(opts)).tools,\n listToolsResult,\n callTool,\n }\n}\n\n/** inputSchema 의 x-mcp-header 속성 → Mcp-Param-<Name> 헤더 (SEP-2243). 문자열 · 숫자 · boolean 값만 */\nfunction paramHeaders(tool: RemoteTool, args: Record<string, unknown>): Record<string, string> {\n const props = tool.inputSchema?.properties\n if (!isRecord(props)) return {}\n const out: Record<string, string> = {}\n for (const [key, schema] of Object.entries(props)) {\n if (!isRecord(schema) || typeof schema[\"x-mcp-header\"] !== \"string\") continue\n const value = args[key]\n if (typeof value === \"string\" || typeof value === \"number\" || typeof value === \"boolean\") {\n out[`Mcp-Param-${schema[\"x-mcp-header\"]}`] = String(value)\n }\n }\n return out\n}\n\n/* SSE */\n\n/** 응답 스트림에서 id 의 응답 메시지가 올 때까지 읽는다. 알림은 콜백, 응답 후 스트림을 닫는다 */\nasync function readSse(res: Response, id: number, opts: RequestOptions): Promise<unknown> {\n if (!res.body) throw new RemoteTransportError(\"Empty event stream.\")\n const reader = res.body.pipeThrough(new TextDecoderStream()).getReader()\n let buffer = \"\"\n let data: string[] = []\n const dispatch = (): unknown => {\n if (data.length === 0) return undefined\n const text = data.join(\"\\n\")\n data = []\n let message: unknown\n try {\n message = JSON.parse(text)\n } catch {\n return undefined\n }\n if (!isRecord(message)) return undefined\n if (typeof message.method === \"string\" && message.id === undefined) {\n try {\n opts.onNotification?.(message.method, isRecord(message.params) ? message.params : {})\n } catch {\n // 표시 오류는 응답에 영향 없음\n }\n return undefined\n }\n if (message.id === id && (\"result\" in message || \"error\" in message)) return message\n return undefined\n }\n try {\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n buffer += value\n let nl: number\n while ((nl = buffer.search(/\\r\\n|\\r|\\n/)) >= 0) {\n const line = buffer.slice(0, nl)\n buffer = buffer.slice(nl + (buffer.startsWith(\"\\r\\n\", nl) ? 2 : 1))\n if (line === \"\") {\n const found = dispatch()\n if (found !== undefined) return found\n continue\n }\n if (line.startsWith(\":\")) continue\n const colon = line.indexOf(\":\")\n const field = colon < 0 ? line : line.slice(0, colon)\n let value = colon < 0 ? \"\" : line.slice(colon + 1)\n if (value.startsWith(\" \")) value = value.slice(1)\n if (field === \"data\") data.push(value)\n }\n }\n const found = dispatch()\n if (found !== undefined) return found\n } finally {\n await reader.cancel().catch(() => {})\n }\n if (opts.signal?.aborted) throw opts.signal.reason\n throw new RemoteTransportError(\"Event stream ended without a response.\")\n}\n\nasync function readJson(res: Response): Promise<unknown> {\n const text = await res.text().catch(() => \"\")\n if (text === \"\") return undefined\n try {\n return JSON.parse(text)\n } catch {\n return undefined\n }\n}\n\n/* WWW-Authenticate */\n\nexport interface BearerChallenge {\n /** Bearer challenge 가 있었는지 */\n bearer: boolean\n error?: string\n scope?: string\n resourceMetadata?: string\n}\n\n/** WWW-Authenticate 의 Bearer challenge 파라미터 (RFC 6750 · RFC 9728 resource_metadata) */\nexport function parseBearerChallenge(header: string | null): BearerChallenge {\n if (!header) return { bearer: false }\n const m = /(?:^|,\\s*)Bearer\\b(.*)$/i.exec(header)\n if (!m) return { bearer: false }\n const params: Record<string, string> = {}\n const re = /([A-Za-z_][\\w-]*)\\s*=\\s*(?:\"((?:[^\"\\\\]|\\\\.)*)\"|([^\\s,]+))/g\n for (let p = re.exec(m[1]!); p !== null; p = re.exec(m[1]!)) {\n params[p[1]!.toLowerCase()] = p[2] !== undefined ? p[2].replace(/\\\\(.)/g, \"$1\") : p[3]!\n }\n const out: BearerChallenge = { bearer: true }\n if (params.error !== undefined) out.error = params.error\n if (params.scope !== undefined) out.scope = params.scope\n if (params.resource_metadata !== undefined) out.resourceMetadata = params.resource_metadata\n return out\n}\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n}\n\nfunction errorMessage(error: unknown): string {\n if (error instanceof Error) {\n const cause = (error as { cause?: unknown }).cause\n return cause instanceof Error && cause.message ? cause.message : error.message\n }\n return String(error)\n}\n"]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 서버 URL 별 토큰 저장 (D6). OS 키체인 = 시스템 CLI (child_process, 네이티브 의존성 없음):
|
|
3
|
+
* macOS `security` (login keychain, generic password) · Linux `secret-tool` (Secret Service).
|
|
4
|
+
* 키체인을 쓸 수 없으면 (그 외 OS · 명령 없음 · 실패) 또는 <BIN>_NO_KEYRING 이면 ~/.config/<bin>/credentials.json (0600).
|
|
5
|
+
* <BIN>_CREDENTIALS_FILE 환경변수 = 그 파일만 사용 (키체인 · 홈 디렉터리 미사용. 테스트 · CI 용).
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* 저장 항목. OAuth (login) = issuer · token_endpoint · client_id · resource 가 있다.
|
|
9
|
+
* API key (login --with-token) = access_token (· token_type) 만.
|
|
10
|
+
*/
|
|
11
|
+
export interface StoredCredentials {
|
|
12
|
+
access_token: string;
|
|
13
|
+
token_type?: string;
|
|
14
|
+
refresh_token?: string;
|
|
15
|
+
/** epoch ms. 없으면 만료 모름 */
|
|
16
|
+
expires_at?: number;
|
|
17
|
+
scope?: string;
|
|
18
|
+
/** 토큰을 발급한 인가 서버 (RFC 8414 issuer) */
|
|
19
|
+
issuer?: string;
|
|
20
|
+
token_endpoint?: string;
|
|
21
|
+
client_id?: string;
|
|
22
|
+
client_secret?: string;
|
|
23
|
+
token_endpoint_auth_method?: string;
|
|
24
|
+
/** RFC 8707 resource (토큰 대상 MCP 서버) */
|
|
25
|
+
resource?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface CredentialStore {
|
|
28
|
+
/** 표시용: "macOS keychain" · "Secret Service" · 파일 경로 */
|
|
29
|
+
readonly location: string;
|
|
30
|
+
get(serverUrl: string): Promise<StoredCredentials | undefined>;
|
|
31
|
+
set(serverUrl: string, credentials: StoredCredentials): Promise<void>;
|
|
32
|
+
delete(serverUrl: string): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
export interface CredentialStoreOptions {
|
|
35
|
+
env: Record<string, string | undefined>;
|
|
36
|
+
/** 실행 파일 이름. 환경변수 접두 (<BIN>_CREDENTIALS_FILE · <BIN>_NO_KEYRING) · 설정 폴더 · 키체인 service. 기본 mext */
|
|
37
|
+
bin?: string;
|
|
38
|
+
/** 기본 process.platform */
|
|
39
|
+
platform?: NodeJS.Platform;
|
|
40
|
+
}
|
|
41
|
+
/** 서버 URL 정규화 (저장 키 · RFC 8707 resource): scheme · host 소문자 (URL 파서), fragment 제거, 끝 / 제거 */
|
|
42
|
+
export declare function canonicalServerUrl(url: string): string;
|
|
43
|
+
/** 실행 파일 이름 → 환경변수 접두. mext → MEXT, my-app → MY_APP */
|
|
44
|
+
export declare function envPrefix(bin: string): string;
|
|
45
|
+
/** 환경변수 플래그: 비어 있지 않고 0 · false 가 아니면 켜짐 */
|
|
46
|
+
export declare function envFlag(value: string | undefined): boolean;
|
|
47
|
+
export declare function credentialStore(options: CredentialStoreOptions): CredentialStore;
|
|
48
|
+
export declare function fileStore(path: string): CredentialStore;
|