@obov/mext 0.0.0-stage → 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.
Files changed (155) hide show
  1. package/dist/build-cli.d.ts +39 -0
  2. package/dist/build-cli.js +160 -0
  3. package/dist/build-cli.js.map +1 -0
  4. package/dist/build.d.ts +65 -0
  5. package/dist/build.js +252 -0
  6. package/dist/build.js.map +1 -0
  7. package/dist/cli/args.d.ts +107 -0
  8. package/dist/cli/args.js +621 -0
  9. package/dist/cli/args.js.map +1 -0
  10. package/dist/cli/bin.d.ts +2 -0
  11. package/dist/cli/bin.js +13 -0
  12. package/dist/cli/bin.js.map +1 -0
  13. package/dist/cli/build.d.ts +11 -0
  14. package/dist/cli/build.js +87 -0
  15. package/dist/cli/build.js.map +1 -0
  16. package/dist/cli/dev-lock.d.ts +23 -0
  17. package/dist/cli/dev-lock.js +68 -0
  18. package/dist/cli/dev-lock.js.map +1 -0
  19. package/dist/cli/dev-server.d.ts +20 -0
  20. package/dist/cli/dev-server.js +74 -0
  21. package/dist/cli/dev-server.js.map +1 -0
  22. package/dist/cli/dev.d.ts +21 -0
  23. package/dist/cli/dev.js +564 -0
  24. package/dist/cli/dev.js.map +1 -0
  25. package/dist/cli/help.d.ts +31 -0
  26. package/dist/cli/help.js +238 -0
  27. package/dist/cli/help.js.map +1 -0
  28. package/dist/cli/io.d.ts +26 -0
  29. package/dist/cli/io.js +74 -0
  30. package/dist/cli/io.js.map +1 -0
  31. package/dist/cli/login.d.ts +32 -0
  32. package/dist/cli/login.js +284 -0
  33. package/dist/cli/login.js.map +1 -0
  34. package/dist/cli/logout.d.ts +9 -0
  35. package/dist/cli/logout.js +105 -0
  36. package/dist/cli/logout.js.map +1 -0
  37. package/dist/cli/main.d.ts +19 -0
  38. package/dist/cli/main.js +77 -0
  39. package/dist/cli/main.js.map +1 -0
  40. package/dist/cli/mcp.d.ts +16 -0
  41. package/dist/cli/mcp.js +92 -0
  42. package/dist/cli/mcp.js.map +1 -0
  43. package/dist/cli/output.d.ts +34 -0
  44. package/dist/cli/output.js +172 -0
  45. package/dist/cli/output.js.map +1 -0
  46. package/dist/cli/remote.d.ts +17 -0
  47. package/dist/cli/remote.js +35 -0
  48. package/dist/cli/remote.js.map +1 -0
  49. package/dist/cli/run.d.ts +14 -0
  50. package/dist/cli/run.js +218 -0
  51. package/dist/cli/run.js.map +1 -0
  52. package/dist/cli/start.d.ts +8 -0
  53. package/dist/cli/start.js +37 -0
  54. package/dist/cli/start.js.map +1 -0
  55. package/dist/client/cache.d.ts +104 -0
  56. package/dist/client/cache.js +208 -0
  57. package/dist/client/cache.js.map +1 -0
  58. package/dist/client/main.d.ts +25 -0
  59. package/dist/client/main.js +100 -0
  60. package/dist/client/main.js.map +1 -0
  61. package/dist/client/manifest.d.ts +19 -0
  62. package/dist/client/manifest.js +68 -0
  63. package/dist/client/manifest.js.map +1 -0
  64. package/dist/client/runner.d.ts +49 -0
  65. package/dist/client/runner.js +562 -0
  66. package/dist/client/runner.js.map +1 -0
  67. package/dist/command.d.ts +75 -0
  68. package/dist/command.js +44 -0
  69. package/dist/command.js.map +1 -0
  70. package/dist/config.d.ts +53 -0
  71. package/dist/config.js +122 -0
  72. package/dist/config.js.map +1 -0
  73. package/dist/errors.d.ts +74 -0
  74. package/dist/errors.js +38 -0
  75. package/dist/errors.js.map +1 -0
  76. package/dist/ids.d.ts +6 -0
  77. package/dist/ids.js +12 -0
  78. package/dist/ids.js.map +1 -0
  79. package/dist/index.d.ts +45 -0
  80. package/dist/index.js +27 -0
  81. package/dist/index.js.map +1 -0
  82. package/dist/instrumentation.d.ts +97 -0
  83. package/dist/instrumentation.js +165 -0
  84. package/dist/instrumentation.js.map +1 -0
  85. package/dist/invoke.d.ts +64 -0
  86. package/dist/invoke.js +152 -0
  87. package/dist/invoke.js.map +1 -0
  88. package/dist/lookup.d.ts +9 -0
  89. package/dist/lookup.js +27 -0
  90. package/dist/lookup.js.map +1 -0
  91. package/dist/manifest.d.ts +68 -0
  92. package/dist/manifest.js +77 -0
  93. package/dist/manifest.js.map +1 -0
  94. package/dist/mcp/constants.d.ts +9 -0
  95. package/dist/mcp/constants.js +10 -0
  96. package/dist/mcp/constants.js.map +1 -0
  97. package/dist/mcp/dispatch.d.ts +66 -0
  98. package/dist/mcp/dispatch.js +249 -0
  99. package/dist/mcp/dispatch.js.map +1 -0
  100. package/dist/mcp/help.d.ts +57 -0
  101. package/dist/mcp/help.js +136 -0
  102. package/dist/mcp/help.js.map +1 -0
  103. package/dist/mcp/http.d.ts +92 -0
  104. package/dist/mcp/http.js +366 -0
  105. package/dist/mcp/http.js.map +1 -0
  106. package/dist/mcp/index.d.ts +15 -0
  107. package/dist/mcp/index.js +9 -0
  108. package/dist/mcp/index.js.map +1 -0
  109. package/dist/mcp/install.d.ts +17 -0
  110. package/dist/mcp/install.js +36 -0
  111. package/dist/mcp/install.js.map +1 -0
  112. package/dist/mcp/mrtr.d.ts +82 -0
  113. package/dist/mcp/mrtr.js +130 -0
  114. package/dist/mcp/mrtr.js.map +1 -0
  115. package/dist/mcp/server.d.ts +58 -0
  116. package/dist/mcp/server.js +204 -0
  117. package/dist/mcp/server.js.map +1 -0
  118. package/dist/mcp/stdio.d.ts +57 -0
  119. package/dist/mcp/stdio.js +79 -0
  120. package/dist/mcp/stdio.js.map +1 -0
  121. package/dist/principal.d.ts +16 -0
  122. package/dist/principal.js +24 -0
  123. package/dist/principal.js.map +1 -0
  124. package/dist/registry.d.ts +90 -0
  125. package/dist/registry.js +169 -0
  126. package/dist/registry.js.map +1 -0
  127. package/dist/remote/browser.d.ts +15 -0
  128. package/dist/remote/browser.js +47 -0
  129. package/dist/remote/browser.js.map +1 -0
  130. package/dist/remote/client.d.ts +113 -0
  131. package/dist/remote/client.js +312 -0
  132. package/dist/remote/client.js.map +1 -0
  133. package/dist/remote/credentials.d.ts +48 -0
  134. package/dist/remote/credentials.js +196 -0
  135. package/dist/remote/credentials.js.map +1 -0
  136. package/dist/remote/index.d.ts +8 -0
  137. package/dist/remote/index.js +5 -0
  138. package/dist/remote/index.js.map +1 -0
  139. package/dist/remote/oauth.d.ts +186 -0
  140. package/dist/remote/oauth.js +507 -0
  141. package/dist/remote/oauth.js.map +1 -0
  142. package/dist/scan.d.ts +34 -0
  143. package/dist/scan.js +155 -0
  144. package/dist/scan.js.map +1 -0
  145. package/dist/start.d.ts +63 -0
  146. package/dist/start.js +267 -0
  147. package/dist/start.js.map +1 -0
  148. package/dist/suggest.d.ts +7 -0
  149. package/dist/suggest.js +45 -0
  150. package/dist/suggest.js.map +1 -0
  151. package/dist/version.d.ts +4 -0
  152. package/dist/version.js +13 -0
  153. package/dist/version.js.map +1 -0
  154. package/package.json +49 -4
  155. package/README.md +0 -3
@@ -0,0 +1,24 @@
1
+ import { userInfo } from "node:os";
2
+ /** 인증 없는 주체. scopes 없음 → readOnly 명령어만 */
3
+ export const ANONYMOUS = Object.freeze({
4
+ id: "anonymous",
5
+ kind: "anonymous",
6
+ scopes: Object.freeze([]),
7
+ });
8
+ /** 로컬 · stdio 실행의 기본 주체: { id: "local:<OS 사용자>", kind: "local", scopes: ["*"] } */
9
+ export function localPrincipal(user = osUser()) {
10
+ return { id: `local:${user}`, kind: "local", scopes: ["*"] };
11
+ }
12
+ /** "*" = 전체 */
13
+ export function hasScope(principal, scope) {
14
+ return principal.scopes.includes("*") || principal.scopes.includes(scope);
15
+ }
16
+ function osUser() {
17
+ try {
18
+ return userInfo().username;
19
+ }
20
+ catch {
21
+ return process.env.USER ?? process.env.USERNAME ?? "unknown";
22
+ }
23
+ }
24
+ //# sourceMappingURL=principal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"principal.js","sourceRoot":"","sources":["../src/principal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAalC,0CAA0C;AAC1C,MAAM,CAAC,MAAM,SAAS,GAAc,MAAM,CAAC,MAAM,CAAC;IAChD,EAAE,EAAE,WAAW;IACf,IAAI,EAAE,WAAW;IACjB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,CAAwB;CACjD,CAAc,CAAA;AAEf,mFAAmF;AACnF,MAAM,UAAU,cAAc,CAAC,IAAI,GAAW,MAAM,EAAE;IACpD,OAAO,EAAE,EAAE,EAAE,SAAS,IAAI,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAA;AAC9D,CAAC;AAED,eAAe;AACf,MAAM,UAAU,QAAQ,CAAC,SAAoB,EAAE,KAAa;IAC1D,OAAO,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;AAC3E,CAAC;AAED,SAAS,MAAM;IACb,IAAI,CAAC;QACH,OAAO,QAAQ,EAAE,CAAC,QAAQ,CAAA;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,SAAS,CAAA;IAC9D,CAAC;AACH,CAAC","sourcesContent":["import { userInfo } from \"node:os\"\n\n/** 요청 주체. 서버가 요청마다 생성 (HTTP · CLI 원격), 로컬 · stdio = kind local */\nexport interface Principal {\n /** \"local:obov\" | \"user_123\" | \"anonymous\" */\n id: string\n kind: \"local\" | \"token\" | \"anonymous\"\n /** \"*\" = 전체 */\n scopes: string[]\n /** 검증된 토큰 내용. 토큰 원문은 포함하지 않는다 */\n claims?: Record<string, unknown>\n}\n\n/** 인증 없는 주체. scopes 없음 → readOnly 명령어만 */\nexport const ANONYMOUS: Principal = Object.freeze({\n id: \"anonymous\",\n kind: \"anonymous\",\n scopes: Object.freeze([]) as unknown as string[],\n}) as Principal\n\n/** 로컬 · stdio 실행의 기본 주체: { id: \"local:<OS 사용자>\", kind: \"local\", scopes: [\"*\"] } */\nexport function localPrincipal(user: string = osUser()): Principal {\n return { id: `local:${user}`, kind: \"local\", scopes: [\"*\"] }\n}\n\n/** \"*\" = 전체 */\nexport function hasScope(principal: Principal, scope: string): boolean {\n return principal.scopes.includes(\"*\") || principal.scopes.includes(scope)\n}\n\nfunction osUser(): string {\n try {\n return userInfo().username\n } catch {\n return process.env.USER ?? process.env.USERNAME ?? \"unknown\"\n }\n}\n"]}
@@ -0,0 +1,90 @@
1
+ import { type Command } from "./command.ts";
2
+ import { type BuildIssue } from "./errors.ts";
3
+ export type JsonSchema = Record<string, unknown>;
4
+ export interface RegistryCommandInput {
5
+ /** 정식 ID. 점 구분 kebab-case 세그먼트 */
6
+ id: string;
7
+ command: Command;
8
+ /** commands/ 기준 상대 경로 (posix). 스캔 결과에만 있음 */
9
+ file?: string;
10
+ }
11
+ export interface RegistryNamespaceInput {
12
+ /** "" = 루트 */
13
+ id: string;
14
+ description?: string;
15
+ }
16
+ export interface RegistryInput {
17
+ commands: readonly RegistryCommandInput[];
18
+ namespaces?: readonly RegistryNamespaceInput[];
19
+ }
20
+ export interface RegisteredCommand {
21
+ id: string;
22
+ /**
23
+ * 메타데이터 (description · 안전성 · scopes · examples · positional · aliases) 의 원본.
24
+ * eager (createRegistry) = 실행 가능한 명령어 그대로. lazy (createLazyRegistry) = manifest 로 만든 메타데이터만
25
+ * (input · execute 는 load() 결과에서)
26
+ */
27
+ command: Command;
28
+ /** lazy Registry: 명령어 모듈 import (1회 캐시) → 실행 가능한 명령어. 없음 = command 가 실행 가능. loadCommand 로 호출 */
29
+ load?: () => Promise<Command>;
30
+ /** 별칭의 전체 ID (정렬) */
31
+ aliases: readonly string[];
32
+ file: string | undefined;
33
+ /** input 의 JSON Schema 2020-12 (io: input) */
34
+ inputSchema: JsonSchema;
35
+ /** output 의 JSON Schema 2020-12 (io: output) */
36
+ outputSchema: JsonSchema | undefined;
37
+ }
38
+ export interface NamespaceInfo {
39
+ /** "" = 루트 */
40
+ id: string;
41
+ /** _index.md 내용 */
42
+ description: string | undefined;
43
+ /** 직속 명령어 정식 ID (정렬). index.ts 명령어 (네임스페이스와 같은 ID) 는 상위 네임스페이스에 속한다 */
44
+ commands: readonly string[];
45
+ /** 직속 하위 네임스페이스 ID (정렬) */
46
+ namespaces: readonly string[];
47
+ }
48
+ export interface Registry {
49
+ /** 정식 ID → 명령어. ID 순 */
50
+ readonly commands: ReadonlyMap<string, RegisteredCommand>;
51
+ /** 별칭 전체 ID → 정식 ID */
52
+ readonly aliases: ReadonlyMap<string, string>;
53
+ /** 네임스페이스 ID → 정보. 루트 ("") 포함, ID 순 */
54
+ readonly namespaces: ReadonlyMap<string, NamespaceInfo>;
55
+ }
56
+ /** 명령어 목록에서 Registry 생성. ID · 별칭 · 스키마 규칙 위반은 MextBuildError */
57
+ export declare function createRegistry(input: RegistryInput): Registry;
58
+ /**
59
+ * createRegistry 의 수집형 (mext build 의 일괄 검증용). 던지지 않는다.
60
+ * registry = 규칙을 통과한 명령어만, issues = 위반 전부
61
+ */
62
+ export declare function collectRegistry(input: RegistryInput): {
63
+ registry: Registry;
64
+ issues: BuildIssue[];
65
+ };
66
+ /** lazy Registry 의 명령어: manifest 메타데이터 + 모듈 로더 (규칙 검증은 mext build 가 이미 함) */
67
+ export interface LazyCommandInput {
68
+ id: string;
69
+ /** 메타데이터만 있는 명령어 (input · execute 는 load 결과). RegisteredCommand.command */
70
+ command: Command;
71
+ /** 별칭의 전체 ID */
72
+ aliases: readonly string[];
73
+ file?: string;
74
+ inputSchema: JsonSchema;
75
+ outputSchema?: JsonSchema;
76
+ /** 실행 시 1회 호출 (결과 캐시, 실패는 다음 호출에서 재시도) */
77
+ load: () => Promise<Command>;
78
+ }
79
+ /**
80
+ * manifest (mext build 산출) 로 Registry 생성. 명령어 모듈을 import 하지 않는다 (mext start · handler.js).
81
+ * help · tools/list · UNKNOWN_COMMAND · 권한 검사 = 메타데이터만, 입력 검증 · execute = loadCommand 후
82
+ */
83
+ export declare function createLazyRegistry(input: {
84
+ commands: readonly LazyCommandInput[];
85
+ namespaces?: readonly RegistryNamespaceInput[];
86
+ }): Registry;
87
+ /** 실행 가능한 명령어. eager = entry.command, lazy = entry.load() (모듈 import) */
88
+ export declare function loadCommand(entry: RegisteredCommand): Promise<Command>;
89
+ /** 정식 ID 또는 별칭으로 명령어 찾기 */
90
+ export declare function resolveCommand(registry: Registry, id: string): RegisteredCommand | undefined;
@@ -0,0 +1,169 @@
1
+ import * as z from "zod";
2
+ import { isCommand } from "./command.js";
3
+ import { MextBuildError } from "./errors.js";
4
+ import { isSegment, parentNamespace, RESERVED_ROOT_SEGMENTS } from "./ids.js";
5
+ /** 명령어 목록에서 Registry 생성. ID · 별칭 · 스키마 규칙 위반은 MextBuildError */
6
+ export function createRegistry(input) {
7
+ const { registry, issues } = collectRegistry(input);
8
+ if (issues.length > 0)
9
+ throw new MextBuildError(issues);
10
+ return registry;
11
+ }
12
+ /**
13
+ * createRegistry 의 수집형 (mext build 의 일괄 검증용). 던지지 않는다.
14
+ * registry = 규칙을 통과한 명령어만, issues = 위반 전부
15
+ */
16
+ export function collectRegistry(input) {
17
+ const issues = [];
18
+ const accepted = [];
19
+ const seen = new Map();
20
+ for (const entry of input.commands) {
21
+ const where = { id: entry.id, file: entry.file };
22
+ if (!isCommand(entry.command)) {
23
+ issues.push({ code: "INVALID_COMMAND", message: "not a defineCommand result", ...where });
24
+ continue;
25
+ }
26
+ const segments = entry.id.split(".");
27
+ const bad = segments.find((s) => !isSegment(s));
28
+ if (bad !== undefined) {
29
+ issues.push({ code: "INVALID_SEGMENT", message: `segment "${bad}" is not kebab-case`, ...where });
30
+ continue;
31
+ }
32
+ if (RESERVED_ROOT_SEGMENTS.includes(segments[0])) {
33
+ issues.push({ code: "RESERVED_ID", message: `root segment "${segments[0]}" is reserved`, ...where });
34
+ continue;
35
+ }
36
+ const prev = seen.get(entry.id);
37
+ if (prev) {
38
+ issues.push({ code: "DUPLICATE_ID", message: `duplicate ID (also ${prev.file ?? prev.id})`, ...where });
39
+ continue;
40
+ }
41
+ seen.set(entry.id, entry);
42
+ accepted.push(entry);
43
+ }
44
+ // 네임스페이스 = 명령어 ID 의 모든 상위 경로
45
+ const namespaceIds = new Set([""]);
46
+ for (const { id } of accepted) {
47
+ for (let ns = parentNamespace(id); ns !== ""; ns = parentNamespace(ns))
48
+ namespaceIds.add(ns);
49
+ }
50
+ const aliases = new Map();
51
+ const aliasesOf = new Map();
52
+ for (const entry of accepted) {
53
+ const where = { id: entry.id, file: entry.file };
54
+ const parent = parentNamespace(entry.id);
55
+ const own = [];
56
+ for (const alias of entry.command.aliases) {
57
+ if (!isSegment(alias)) {
58
+ issues.push({ code: "INVALID_ALIAS", message: `alias "${alias}" is not a kebab-case segment`, ...where });
59
+ continue;
60
+ }
61
+ if (parent === "") {
62
+ issues.push({ code: "INVALID_ALIAS", message: `alias "${alias}": aliases are not allowed at the root`, ...where });
63
+ continue;
64
+ }
65
+ const full = `${parent}.${alias}`;
66
+ const owner = seen.has(full) ? full : aliases.get(full);
67
+ if (owner !== undefined || namespaceIds.has(full)) {
68
+ issues.push({
69
+ code: "DUPLICATE_ID",
70
+ message: `alias ${full} duplicates ${owner !== undefined ? owner : "a namespace"}`,
71
+ ...where,
72
+ });
73
+ continue;
74
+ }
75
+ aliases.set(full, entry.id);
76
+ own.push(full);
77
+ }
78
+ aliasesOf.set(entry.id, own.sort());
79
+ }
80
+ const commands = new Map();
81
+ for (const entry of [...accepted].sort((a, b) => compareIds(a.id, b.id))) {
82
+ const where = { id: entry.id, file: entry.file };
83
+ let inputSchema;
84
+ let outputSchema;
85
+ try {
86
+ inputSchema = toJsonSchema(entry.command.input, "input");
87
+ outputSchema = entry.command.output ? toJsonSchema(entry.command.output, "output") : undefined;
88
+ }
89
+ catch (error) {
90
+ issues.push({ code: "SCHEMA_UNREPRESENTABLE", message: errorMessage(error), ...where });
91
+ continue;
92
+ }
93
+ commands.set(entry.id, {
94
+ id: entry.id,
95
+ command: entry.command,
96
+ aliases: aliasesOf.get(entry.id) ?? [],
97
+ file: entry.file,
98
+ inputSchema,
99
+ outputSchema,
100
+ });
101
+ }
102
+ const namespaces = namespaceTable(namespaceIds, [...commands.keys()], input.namespaces);
103
+ return { registry: { commands, aliases, namespaces }, issues };
104
+ }
105
+ /**
106
+ * manifest (mext build 산출) 로 Registry 생성. 명령어 모듈을 import 하지 않는다 (mext start · handler.js).
107
+ * help · tools/list · UNKNOWN_COMMAND · 권한 검사 = 메타데이터만, 입력 검증 · execute = loadCommand 후
108
+ */
109
+ export function createLazyRegistry(input) {
110
+ const commands = new Map();
111
+ const aliases = new Map();
112
+ for (const entry of [...input.commands].sort((a, b) => compareIds(a.id, b.id))) {
113
+ const load = entry.load;
114
+ let pending;
115
+ commands.set(entry.id, {
116
+ id: entry.id,
117
+ command: entry.command,
118
+ aliases: [...entry.aliases].sort(),
119
+ file: entry.file,
120
+ inputSchema: entry.inputSchema,
121
+ outputSchema: entry.outputSchema,
122
+ load: () => (pending ??= load().catch((error) => {
123
+ pending = undefined;
124
+ throw error;
125
+ })),
126
+ });
127
+ for (const alias of entry.aliases)
128
+ aliases.set(alias, entry.id);
129
+ }
130
+ const namespaceIds = new Set([""]);
131
+ for (const id of commands.keys()) {
132
+ for (let ns = parentNamespace(id); ns !== ""; ns = parentNamespace(ns))
133
+ namespaceIds.add(ns);
134
+ }
135
+ return { commands, aliases, namespaces: namespaceTable(namespaceIds, [...commands.keys()], input.namespaces) };
136
+ }
137
+ /** 실행 가능한 명령어. eager = entry.command, lazy = entry.load() (모듈 import) */
138
+ export function loadCommand(entry) {
139
+ return entry.load ? entry.load() : Promise.resolve(entry.command);
140
+ }
141
+ function namespaceTable(namespaceIds, commandIds, inputs) {
142
+ const descriptions = new Map();
143
+ for (const ns of inputs ?? [])
144
+ descriptions.set(ns.id, ns.description);
145
+ const namespaces = new Map();
146
+ for (const id of [...namespaceIds].sort(compareIds)) {
147
+ namespaces.set(id, {
148
+ id,
149
+ description: descriptions.get(id),
150
+ commands: commandIds.filter((c) => parentNamespace(c) === id),
151
+ namespaces: [...namespaceIds].filter((n) => n !== "" && parentNamespace(n) === id).sort(compareIds),
152
+ });
153
+ }
154
+ return namespaces;
155
+ }
156
+ /** 정식 ID 또는 별칭으로 명령어 찾기 */
157
+ export function resolveCommand(registry, id) {
158
+ return registry.commands.get(id) ?? registry.commands.get(registry.aliases.get(id) ?? "");
159
+ }
160
+ function toJsonSchema(schema, io) {
161
+ return z.toJSONSchema(schema, { target: "draft-2020-12", io, unrepresentable: "throw" });
162
+ }
163
+ function compareIds(a, b) {
164
+ return a < b ? -1 : a > b ? 1 : 0;
165
+ }
166
+ function errorMessage(error) {
167
+ return error instanceof Error ? error.message : String(error);
168
+ }
169
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"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","sourcesContent":["import * as z from \"zod\"\nimport { isCommand, type Command } from \"./command.ts\"\nimport { MextBuildError, type BuildIssue } from \"./errors.ts\"\nimport { isSegment, parentNamespace, RESERVED_ROOT_SEGMENTS } from \"./ids.ts\"\n\nexport type JsonSchema = Record<string, unknown>\n\nexport interface RegistryCommandInput {\n /** 정식 ID. 점 구분 kebab-case 세그먼트 */\n id: string\n command: Command\n /** commands/ 기준 상대 경로 (posix). 스캔 결과에만 있음 */\n file?: string\n}\n\nexport interface RegistryNamespaceInput {\n /** \"\" = 루트 */\n id: string\n description?: string\n}\n\nexport interface RegistryInput {\n commands: readonly RegistryCommandInput[]\n namespaces?: readonly RegistryNamespaceInput[]\n}\n\nexport interface RegisteredCommand {\n id: string\n /**\n * 메타데이터 (description · 안전성 · scopes · examples · positional · aliases) 의 원본.\n * eager (createRegistry) = 실행 가능한 명령어 그대로. lazy (createLazyRegistry) = manifest 로 만든 메타데이터만\n * (input · execute 는 load() 결과에서)\n */\n command: Command\n /** lazy Registry: 명령어 모듈 import (1회 캐시) → 실행 가능한 명령어. 없음 = command 가 실행 가능. loadCommand 로 호출 */\n load?: () => Promise<Command>\n /** 별칭의 전체 ID (정렬) */\n aliases: readonly string[]\n file: string | undefined\n /** input 의 JSON Schema 2020-12 (io: input) */\n inputSchema: JsonSchema\n /** output 의 JSON Schema 2020-12 (io: output) */\n outputSchema: JsonSchema | undefined\n}\n\nexport interface NamespaceInfo {\n /** \"\" = 루트 */\n id: string\n /** _index.md 내용 */\n description: string | undefined\n /** 직속 명령어 정식 ID (정렬). index.ts 명령어 (네임스페이스와 같은 ID) 는 상위 네임스페이스에 속한다 */\n commands: readonly string[]\n /** 직속 하위 네임스페이스 ID (정렬) */\n namespaces: readonly string[]\n}\n\nexport interface Registry {\n /** 정식 ID → 명령어. ID 순 */\n readonly commands: ReadonlyMap<string, RegisteredCommand>\n /** 별칭 전체 ID → 정식 ID */\n readonly aliases: ReadonlyMap<string, string>\n /** 네임스페이스 ID → 정보. 루트 (\"\") 포함, ID 순 */\n readonly namespaces: ReadonlyMap<string, NamespaceInfo>\n}\n\n/** 명령어 목록에서 Registry 생성. ID · 별칭 · 스키마 규칙 위반은 MextBuildError */\nexport function createRegistry(input: RegistryInput): Registry {\n const { registry, issues } = collectRegistry(input)\n if (issues.length > 0) throw new MextBuildError(issues)\n return registry\n}\n\n/**\n * createRegistry 의 수집형 (mext build 의 일괄 검증용). 던지지 않는다.\n * registry = 규칙을 통과한 명령어만, issues = 위반 전부\n */\nexport function collectRegistry(input: RegistryInput): { registry: Registry; issues: BuildIssue[] } {\n const issues: BuildIssue[] = []\n const accepted: RegistryCommandInput[] = []\n const seen = new Map<string, RegistryCommandInput>()\n\n for (const entry of input.commands) {\n const where = { id: entry.id, file: entry.file }\n if (!isCommand(entry.command)) {\n issues.push({ code: \"INVALID_COMMAND\", message: \"not a defineCommand result\", ...where })\n continue\n }\n const segments = entry.id.split(\".\")\n const bad = segments.find((s) => !isSegment(s))\n if (bad !== undefined) {\n issues.push({ code: \"INVALID_SEGMENT\", message: `segment \"${bad}\" is not kebab-case`, ...where })\n continue\n }\n if (RESERVED_ROOT_SEGMENTS.includes(segments[0]!)) {\n issues.push({ code: \"RESERVED_ID\", message: `root segment \"${segments[0]}\" is reserved`, ...where })\n continue\n }\n const prev = seen.get(entry.id)\n if (prev) {\n issues.push({ code: \"DUPLICATE_ID\", message: `duplicate ID (also ${prev.file ?? prev.id})`, ...where })\n continue\n }\n seen.set(entry.id, entry)\n accepted.push(entry)\n }\n\n // 네임스페이스 = 명령어 ID 의 모든 상위 경로\n const namespaceIds = new Set<string>([\"\"])\n for (const { id } of accepted) {\n for (let ns = parentNamespace(id); ns !== \"\"; ns = parentNamespace(ns)) namespaceIds.add(ns)\n }\n\n const aliases = new Map<string, string>()\n const aliasesOf = new Map<string, string[]>()\n for (const entry of accepted) {\n const where = { id: entry.id, file: entry.file }\n const parent = parentNamespace(entry.id)\n const own: string[] = []\n for (const alias of entry.command.aliases) {\n if (!isSegment(alias)) {\n issues.push({ code: \"INVALID_ALIAS\", message: `alias \"${alias}\" is not a kebab-case segment`, ...where })\n continue\n }\n if (parent === \"\") {\n issues.push({ code: \"INVALID_ALIAS\", message: `alias \"${alias}\": aliases are not allowed at the root`, ...where })\n continue\n }\n const full = `${parent}.${alias}`\n const owner = seen.has(full) ? full : aliases.get(full)\n if (owner !== undefined || namespaceIds.has(full)) {\n issues.push({\n code: \"DUPLICATE_ID\",\n message: `alias ${full} duplicates ${owner !== undefined ? owner : \"a namespace\"}`,\n ...where,\n })\n continue\n }\n aliases.set(full, entry.id)\n own.push(full)\n }\n aliasesOf.set(entry.id, own.sort())\n }\n\n const commands = new Map<string, RegisteredCommand>()\n for (const entry of [...accepted].sort((a, b) => compareIds(a.id, b.id))) {\n const where = { id: entry.id, file: entry.file }\n let inputSchema: JsonSchema\n let outputSchema: JsonSchema | undefined\n try {\n inputSchema = toJsonSchema(entry.command.input, \"input\")\n outputSchema = entry.command.output ? toJsonSchema(entry.command.output, \"output\") : undefined\n } catch (error) {\n issues.push({ code: \"SCHEMA_UNREPRESENTABLE\", message: errorMessage(error), ...where })\n continue\n }\n commands.set(entry.id, {\n id: entry.id,\n command: entry.command,\n aliases: aliasesOf.get(entry.id) ?? [],\n file: entry.file,\n inputSchema,\n outputSchema,\n })\n }\n\n const namespaces = namespaceTable(namespaceIds, [...commands.keys()], input.namespaces)\n return { registry: { commands, aliases, namespaces }, issues }\n}\n\n/** lazy Registry 의 명령어: manifest 메타데이터 + 모듈 로더 (규칙 검증은 mext build 가 이미 함) */\nexport interface LazyCommandInput {\n id: string\n /** 메타데이터만 있는 명령어 (input · execute 는 load 결과). RegisteredCommand.command */\n command: Command\n /** 별칭의 전체 ID */\n aliases: readonly string[]\n file?: string\n inputSchema: JsonSchema\n outputSchema?: JsonSchema\n /** 실행 시 1회 호출 (결과 캐시, 실패는 다음 호출에서 재시도) */\n load: () => Promise<Command>\n}\n\n/**\n * manifest (mext build 산출) 로 Registry 생성. 명령어 모듈을 import 하지 않는다 (mext start · handler.js).\n * help · tools/list · UNKNOWN_COMMAND · 권한 검사 = 메타데이터만, 입력 검증 · execute = loadCommand 후\n */\nexport function createLazyRegistry(input: {\n commands: readonly LazyCommandInput[]\n namespaces?: readonly RegistryNamespaceInput[]\n}): Registry {\n const commands = new Map<string, RegisteredCommand>()\n const aliases = new Map<string, string>()\n for (const entry of [...input.commands].sort((a, b) => compareIds(a.id, b.id))) {\n const load = entry.load\n let pending: Promise<Command> | undefined\n commands.set(entry.id, {\n id: entry.id,\n command: entry.command,\n aliases: [...entry.aliases].sort(),\n file: entry.file,\n inputSchema: entry.inputSchema,\n outputSchema: entry.outputSchema,\n load: () =>\n (pending ??= load().catch((error: unknown) => {\n pending = undefined\n throw error\n })),\n })\n for (const alias of entry.aliases) aliases.set(alias, entry.id)\n }\n const namespaceIds = new Set<string>([\"\"])\n for (const id of commands.keys()) {\n for (let ns = parentNamespace(id); ns !== \"\"; ns = parentNamespace(ns)) namespaceIds.add(ns)\n }\n return { commands, aliases, namespaces: namespaceTable(namespaceIds, [...commands.keys()], input.namespaces) }\n}\n\n/** 실행 가능한 명령어. eager = entry.command, lazy = entry.load() (모듈 import) */\nexport function loadCommand(entry: RegisteredCommand): Promise<Command> {\n return entry.load ? entry.load() : Promise.resolve(entry.command)\n}\n\nfunction namespaceTable(\n namespaceIds: ReadonlySet<string>,\n commandIds: readonly string[],\n inputs: readonly RegistryNamespaceInput[] | undefined,\n): Map<string, NamespaceInfo> {\n const descriptions = new Map<string, string | undefined>()\n for (const ns of inputs ?? []) descriptions.set(ns.id, ns.description)\n const namespaces = new Map<string, NamespaceInfo>()\n for (const id of [...namespaceIds].sort(compareIds)) {\n namespaces.set(id, {\n id,\n description: descriptions.get(id),\n commands: commandIds.filter((c) => parentNamespace(c) === id),\n namespaces: [...namespaceIds].filter((n) => n !== \"\" && parentNamespace(n) === id).sort(compareIds),\n })\n }\n return namespaces\n}\n\n/** 정식 ID 또는 별칭으로 명령어 찾기 */\nexport function resolveCommand(registry: Registry, id: string): RegisteredCommand | undefined {\n return registry.commands.get(id) ?? registry.commands.get(registry.aliases.get(id) ?? \"\")\n}\n\nfunction toJsonSchema(schema: z.ZodType, io: \"input\" | \"output\"): JsonSchema {\n return z.toJSONSchema(schema, { target: \"draft-2020-12\", io, unrepresentable: \"throw\" }) as JsonSchema\n}\n\nfunction compareIds(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error)\n}\n"]}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * login 의 브라우저 판정 · 열기 (spec §5 login 브라우저 판정 · headless).
3
+ * 열지 않음 = SSH 세션 (SSH_CONNECTION · SSH_CLIENT · SSH_TTY) · Linux 에서 DISPLAY · WAYLAND_DISPLAY 없음.
4
+ * 여는 명령 = BROWSER (있으면) → open (macOS) · start (Windows) · xdg-open.
5
+ */
6
+ export interface BrowserDecision {
7
+ open: boolean;
8
+ /** 열지 않는 이유 (안내 출력용) */
9
+ reason?: "ssh" | "no-display";
10
+ }
11
+ /** URL 을 브라우저로 연다. 실패는 무시 (URL 은 항상 stderr 에 출력) */
12
+ export type BrowserOpener = (url: string) => void;
13
+ export declare function browserDecision(env: Record<string, string | undefined>, platform?: NodeJS.Platform): BrowserDecision;
14
+ /** 기본 opener. BROWSER = 명령 (공백 구분 인자, %s 가 있으면 URL 로 치환 · 없으면 끝에 URL) */
15
+ export declare function systemBrowserOpener(env: Record<string, string | undefined>, platform?: NodeJS.Platform): BrowserOpener;
@@ -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>;