@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.
- package/dist/build-cli.d.ts +39 -0
- package/dist/build-cli.js +160 -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 +74 -0
- package/dist/cli/dev-server.js.map +1 -0
- package/dist/cli/dev.d.ts +21 -0
- package/dist/cli/dev.js +564 -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 +34 -0
- package/dist/cli/output.js +172 -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 +25 -0
- package/dist/client/main.js +100 -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 +49 -0
- package/dist/client/runner.js +562 -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 +53 -0
- package/dist/config.js +122 -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 +45 -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 +92 -0
- package/dist/mcp/http.js +366 -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 +58 -0
- package/dist/mcp/server.js +204 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/stdio.d.ts +57 -0
- package/dist/mcp/stdio.js +79 -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 +267 -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
package/dist/mcp/mrtr.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { inputDeclined } from "../errors.js";
|
|
3
|
+
export const ASK_KEY_PREFIX = "ask-";
|
|
4
|
+
/** confirm 요청의 requestedSchema. 승인 = action accept + content.confirm === true */
|
|
5
|
+
export const CONFIRM_REQUESTED_SCHEMA = {
|
|
6
|
+
type: "object",
|
|
7
|
+
properties: { confirm: { type: "boolean", title: "Confirm", description: "true to proceed" } },
|
|
8
|
+
required: ["confirm"],
|
|
9
|
+
};
|
|
10
|
+
/** requestState 유효 기간 (사람이 답하는 시간) */
|
|
11
|
+
export const REQUEST_STATE_TTL_MS = 600_000;
|
|
12
|
+
const STATE_VERSION = "m1";
|
|
13
|
+
export function createRequestStateSigner(options = {}) {
|
|
14
|
+
const key = options.key === undefined ? randomBytes(32) : Buffer.from(options.key);
|
|
15
|
+
if (key.length < 32)
|
|
16
|
+
throw new RangeError("requestStateKey must be at least 32 bytes");
|
|
17
|
+
const ttlMs = options.ttlMs ?? REQUEST_STATE_TTL_MS;
|
|
18
|
+
const now = options.now ?? Date.now;
|
|
19
|
+
const mac = (body) => createHmac("sha256", key).update(`mext.requestState:${body}`).digest();
|
|
20
|
+
return {
|
|
21
|
+
mint(answers, binding) {
|
|
22
|
+
const body = b64url(JSON.stringify({ exp: now() + ttlMs, b: bindingDigest(binding), a: answers }));
|
|
23
|
+
return `${STATE_VERSION}.${body}.${b64url(mac(body))}`;
|
|
24
|
+
},
|
|
25
|
+
verify(state, binding) {
|
|
26
|
+
const [version, body, signature, ...rest] = state.split(".");
|
|
27
|
+
if (version !== STATE_VERSION || body === undefined || signature === undefined || rest.length > 0)
|
|
28
|
+
return undefined;
|
|
29
|
+
const expected = mac(body);
|
|
30
|
+
const given = Buffer.from(signature, "base64url");
|
|
31
|
+
if (given.length !== expected.length || !timingSafeEqual(given, expected))
|
|
32
|
+
return undefined;
|
|
33
|
+
let payload;
|
|
34
|
+
try {
|
|
35
|
+
payload = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
if (typeof payload.exp !== "number" || payload.exp < now())
|
|
41
|
+
return undefined;
|
|
42
|
+
if (payload.b !== bindingDigest(binding))
|
|
43
|
+
return undefined;
|
|
44
|
+
return isPlainObject(payload.a) ? payload.a : undefined;
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** 대기 요청이 생겨 라운드를 멈출 때 ask 가 던지는 값. 핸들러가 잡아도 결과는 InputRequiredResult */
|
|
49
|
+
export class InputRequiredSignal extends Error {
|
|
50
|
+
constructor() {
|
|
51
|
+
super("Input required: this round stops and the client retries with inputResponses.");
|
|
52
|
+
this.name = "InputRequiredSignal";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** 한 라운드의 ask 세션. answers = 재시도의 inputResponses + 검증된 requestState 의 답 */
|
|
56
|
+
export function createAskSession(answers) {
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
const pending = {};
|
|
59
|
+
const used = {};
|
|
60
|
+
let index = 0;
|
|
61
|
+
const ask = async (request) => {
|
|
62
|
+
const key = `${ASK_KEY_PREFIX}${index++}`;
|
|
63
|
+
const response = elicitResult(answers[key]);
|
|
64
|
+
if (response === undefined) {
|
|
65
|
+
pending[key] = elicitation(request);
|
|
66
|
+
const signal = new InputRequiredSignal();
|
|
67
|
+
controller.abort(signal);
|
|
68
|
+
throw signal;
|
|
69
|
+
}
|
|
70
|
+
used[key] = answers[key];
|
|
71
|
+
if (request.kind === "confirm")
|
|
72
|
+
return response.action === "accept" && response.content?.confirm === true;
|
|
73
|
+
if (response.action !== "accept")
|
|
74
|
+
throw inputDeclined(request.command, "input");
|
|
75
|
+
return response.content ?? {};
|
|
76
|
+
};
|
|
77
|
+
return { ask, pending, used, signal: controller.signal };
|
|
78
|
+
}
|
|
79
|
+
export function hasPending(session) {
|
|
80
|
+
return Object.keys(session.pending).length > 0;
|
|
81
|
+
}
|
|
82
|
+
/** 대기 요청 → InputRequiredResult. 사용한 답이 있으면 서명한 requestState 로 다음 라운드에 넘긴다 */
|
|
83
|
+
export function inputRequiredResult(session, signer, binding) {
|
|
84
|
+
const result = { resultType: "input_required", inputRequests: { ...session.pending } };
|
|
85
|
+
if (Object.keys(session.used).length > 0)
|
|
86
|
+
result.requestState = signer.mint({ ...session.used }, binding);
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
/** elicitation 결과 형태 (action) 만 답으로 인정. 그 외 = 답 없음 → 다시 요청 */
|
|
90
|
+
function elicitResult(value) {
|
|
91
|
+
if (!isPlainObject(value))
|
|
92
|
+
return undefined;
|
|
93
|
+
const { action, content } = value;
|
|
94
|
+
if (action !== "accept" && action !== "decline" && action !== "cancel")
|
|
95
|
+
return undefined;
|
|
96
|
+
return isPlainObject(content) ? { action, content } : { action };
|
|
97
|
+
}
|
|
98
|
+
function elicitation(request) {
|
|
99
|
+
const requestedSchema = request.kind === "confirm" ? CONFIRM_REQUESTED_SCHEMA : elicitationSchema(request.requestedSchema);
|
|
100
|
+
return { method: "elicitation/create", params: { mode: "form", message: request.message, requestedSchema } };
|
|
101
|
+
}
|
|
102
|
+
/** elicitation requestedSchema 는 $schema 를 제외한 평탄 객체 스키마 */
|
|
103
|
+
function elicitationSchema(schema) {
|
|
104
|
+
const { $schema: _schema, ...rest } = schema;
|
|
105
|
+
return rest;
|
|
106
|
+
}
|
|
107
|
+
function bindingDigest(binding) {
|
|
108
|
+
const text = stableStringify([binding.principal, binding.tool, binding.command, binding.args ?? {}]);
|
|
109
|
+
return createHash("sha256").update(text).digest("base64url");
|
|
110
|
+
}
|
|
111
|
+
/** 키 정렬 JSON (args 다이제스트용) */
|
|
112
|
+
function stableStringify(value) {
|
|
113
|
+
if (Array.isArray(value))
|
|
114
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
115
|
+
if (isPlainObject(value)) {
|
|
116
|
+
const entries = Object.keys(value)
|
|
117
|
+
.sort()
|
|
118
|
+
.filter((k) => value[k] !== undefined)
|
|
119
|
+
.map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`);
|
|
120
|
+
return `{${entries.join(",")}}`;
|
|
121
|
+
}
|
|
122
|
+
return JSON.stringify(value) ?? "null";
|
|
123
|
+
}
|
|
124
|
+
function b64url(value) {
|
|
125
|
+
return Buffer.from(value).toString("base64url");
|
|
126
|
+
}
|
|
127
|
+
function isPlainObject(value) {
|
|
128
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=mrtr.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mrtr.js","sourceRoot":"","sources":["../../src/mcp/mrtr.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash, createHmac, randomBytes, timingSafeEqual } from \"node:crypto\"\nimport { inputDeclined } from \"../errors.ts\"\nimport type { AskHandler, AskHandlerRequest } from \"../invoke.ts\"\nimport type { JsonSchema } from \"../registry.ts\"\n\n/**\n * ctx.ask 의 MCP 구현: Multi Round-Trip Requests (MCP 2026-07-28, basic/patterns/mrtr). transport · SDK 무관.\n *\n * stateless: 라운드마다 핸들러를 처음부터 다시 실행한다. ctx.ask 는 호출 순서대로 키 ask-0 · ask-1 · … 를 받고,\n * 답 (재시도의 inputResponses · 이전 라운드의 requestState) 이 있으면 그 값으로 resolve, 없으면 라운드를 멈추고\n * InputRequiredResult (elicitation/create form) 를 돌려준다. 서버 메모리에 대기 상태를 두지 않는다.\n * 그래서 ctx.ask 이전의 핸들러 코드는 라운드마다 다시 실행된다 (부작용은 ask 뒤에 둔다).\n *\n * requestState = 이전 라운드에서 받은 답 (2번째 이후 ask 에만 필요). HMAC-SHA256 서명 · 만료 · 바인딩\n * (principal · 도구 · 명령어 · args 다이제스트). 서명만 (암호화 아님): 클라이언트 자신의 답만 담는다.\n */\n\n/** 임베드 elicitation 요청 (InputRequests 의 값) */\nexport interface ElicitationRequest {\n method: \"elicitation/create\"\n params: { mode: \"form\"; message: string; requestedSchema: JsonSchema }\n}\n\n/** MCP InputRequiredResult (tools/call 결과). SDK 타입 비의존 */\nexport interface InputRequiredResult {\n resultType: \"input_required\"\n /** 키 = ask-<호출 순서>. 클라이언트는 같은 키로 inputResponses 를 채워 같은 tools/call 을 재시도 */\n inputRequests: Record<string, ElicitationRequest>\n /** 있으면 재시도에 그대로 echo */\n requestState?: string\n}\n\n/** 한 라운드의 MRTR 입력. transport 가 요청에서 채운다 */\nexport interface InputRound {\n /** 요청의 clientCapabilities 에 elicitation (form) 선언. false = ctx.ask → INPUT_UNAVAILABLE */\n elicitation: boolean\n /** 재시도의 inputResponses (bare ElicitResult). 첫 호출 = undefined */\n inputResponses?: Record<string, unknown>\n /** 재시도의 requestState 원문 */\n requestState?: string\n}\n\nexport const ASK_KEY_PREFIX = \"ask-\"\n\n/** confirm 요청의 requestedSchema. 승인 = action accept + content.confirm === true */\nexport const CONFIRM_REQUESTED_SCHEMA: JsonSchema = {\n type: \"object\",\n properties: { confirm: { type: \"boolean\", title: \"Confirm\", description: \"true to proceed\" } },\n required: [\"confirm\"],\n}\n\n/** requestState 유효 기간 (사람이 답하는 시간) */\nexport const REQUEST_STATE_TTL_MS = 600_000\n\n/** requestState 가 묶이는 호출. 다른 주체 · 도구 · 명령어 · args 에서 제시하면 거부 */\nexport interface RequestStateBinding {\n principal: string\n tool: string\n command: string\n args: unknown\n}\n\nexport interface RequestStateSigner {\n mint(answers: Record<string, unknown>, binding: RequestStateBinding): string\n /** 검증된 답. 서명 · 만료 · 바인딩 불일치 · 형식 오류 = undefined */\n verify(state: string, binding: RequestStateBinding): Record<string, unknown> | undefined\n}\n\nexport interface RequestStateSignerOptions {\n /** HMAC 키, 32바이트 이상. 기본 = 프로세스별 임의 키 (여러 인스턴스 배포는 공유 키 필요) */\n key?: string | Uint8Array\n ttlMs?: number\n now?: () => number\n}\n\nconst STATE_VERSION = \"m1\"\n\nexport function createRequestStateSigner(options: RequestStateSignerOptions = {}): RequestStateSigner {\n const key = options.key === undefined ? randomBytes(32) : Buffer.from(options.key)\n if (key.length < 32) throw new RangeError(\"requestStateKey must be at least 32 bytes\")\n const ttlMs = options.ttlMs ?? REQUEST_STATE_TTL_MS\n const now = options.now ?? Date.now\n const mac = (body: string) => createHmac(\"sha256\", key).update(`mext.requestState:${body}`).digest()\n return {\n mint(answers, binding) {\n const body = b64url(JSON.stringify({ exp: now() + ttlMs, b: bindingDigest(binding), a: answers }))\n return `${STATE_VERSION}.${body}.${b64url(mac(body))}`\n },\n verify(state, binding) {\n const [version, body, signature, ...rest] = state.split(\".\")\n if (version !== STATE_VERSION || body === undefined || signature === undefined || rest.length > 0) return undefined\n const expected = mac(body)\n const given = Buffer.from(signature, \"base64url\")\n if (given.length !== expected.length || !timingSafeEqual(given, expected)) return undefined\n let payload: { exp?: unknown; b?: unknown; a?: unknown }\n try {\n payload = JSON.parse(Buffer.from(body, \"base64url\").toString(\"utf8\"))\n } catch {\n return undefined\n }\n if (typeof payload.exp !== \"number\" || payload.exp < now()) return undefined\n if (payload.b !== bindingDigest(binding)) return undefined\n return isPlainObject(payload.a) ? payload.a : undefined\n },\n }\n}\n\nexport interface AskSession {\n /** invoke 의 ask 로 주입 */\n ask: AskHandler\n /** 답이 없어 이번 라운드를 멈춘 요청 */\n readonly pending: Record<string, ElicitationRequest>\n /** 이번 라운드에서 사용한 답 (다음 requestState 에 실림) */\n readonly used: Record<string, unknown>\n /** 대기 요청이 생기면 abort (핸들러 signal 에 연결) */\n readonly signal: AbortSignal\n}\n\n/** 대기 요청이 생겨 라운드를 멈출 때 ask 가 던지는 값. 핸들러가 잡아도 결과는 InputRequiredResult */\nexport class InputRequiredSignal extends Error {\n constructor() {\n super(\"Input required: this round stops and the client retries with inputResponses.\")\n this.name = \"InputRequiredSignal\"\n }\n}\n\n/** 한 라운드의 ask 세션. answers = 재시도의 inputResponses + 검증된 requestState 의 답 */\nexport function createAskSession(answers: Record<string, unknown>): AskSession {\n const controller = new AbortController()\n const pending: Record<string, ElicitationRequest> = {}\n const used: Record<string, unknown> = {}\n let index = 0\n const ask: AskHandler = async (request) => {\n const key = `${ASK_KEY_PREFIX}${index++}`\n const response = elicitResult(answers[key])\n if (response === undefined) {\n pending[key] = elicitation(request)\n const signal = new InputRequiredSignal()\n controller.abort(signal)\n throw signal\n }\n used[key] = answers[key]\n if (request.kind === \"confirm\") return response.action === \"accept\" && response.content?.confirm === true\n if (response.action !== \"accept\") throw inputDeclined(request.command, \"input\")\n return response.content ?? {}\n }\n return { ask, pending, used, signal: controller.signal }\n}\n\nexport function hasPending(session: AskSession): boolean {\n return Object.keys(session.pending).length > 0\n}\n\n/** 대기 요청 → InputRequiredResult. 사용한 답이 있으면 서명한 requestState 로 다음 라운드에 넘긴다 */\nexport function inputRequiredResult(\n session: AskSession,\n signer: RequestStateSigner,\n binding: RequestStateBinding,\n): InputRequiredResult {\n const result: InputRequiredResult = { resultType: \"input_required\", inputRequests: { ...session.pending } }\n if (Object.keys(session.used).length > 0) result.requestState = signer.mint({ ...session.used }, binding)\n return result\n}\n\n/** elicitation 결과 형태 (action) 만 답으로 인정. 그 외 = 답 없음 → 다시 요청 */\nfunction elicitResult(value: unknown): { action: \"accept\" | \"decline\" | \"cancel\"; content?: Record<string, unknown> } | undefined {\n if (!isPlainObject(value)) return undefined\n const { action, content } = value\n if (action !== \"accept\" && action !== \"decline\" && action !== \"cancel\") return undefined\n return isPlainObject(content) ? { action, content } : { action }\n}\n\nfunction elicitation(request: AskHandlerRequest): ElicitationRequest {\n const requestedSchema = request.kind === \"confirm\" ? CONFIRM_REQUESTED_SCHEMA : elicitationSchema(request.requestedSchema)\n return { method: \"elicitation/create\", params: { mode: \"form\", message: request.message, requestedSchema } }\n}\n\n/** elicitation requestedSchema 는 $schema 를 제외한 평탄 객체 스키마 */\nfunction elicitationSchema(schema: JsonSchema): JsonSchema {\n const { $schema: _schema, ...rest } = schema\n return rest\n}\n\nfunction bindingDigest(binding: RequestStateBinding): string {\n const text = stableStringify([binding.principal, binding.tool, binding.command, binding.args ?? {}])\n return createHash(\"sha256\").update(text).digest(\"base64url\")\n}\n\n/** 키 정렬 JSON (args 다이제스트용) */\nfunction stableStringify(value: unknown): string {\n if (Array.isArray(value)) return `[${value.map(stableStringify).join(\",\")}]`\n if (isPlainObject(value)) {\n const entries = Object.keys(value)\n .sort()\n .filter((k) => value[k] !== undefined)\n .map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`)\n return `{${entries.join(\",\")}}`\n }\n return JSON.stringify(value) ?? \"null\"\n}\n\nfunction b64url(value: string | Buffer): string {\n return Buffer.from(value).toString(\"base64url\")\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n}\n"]}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type AuthInfo, type McpRequestContext, type McpServerFactory } from "@modelcontextprotocol/server";
|
|
2
|
+
import type { MextApp } from "../command.ts";
|
|
3
|
+
import type { Instrumentation, Transport } from "../instrumentation.ts";
|
|
4
|
+
import { type Principal } from "../principal.ts";
|
|
5
|
+
import type { Registry } from "../registry.ts";
|
|
6
|
+
import { type ToolExposure } from "./dispatch.ts";
|
|
7
|
+
/** tools/list · server/discover 캐시 힌트. 도구 정의는 principal 과 무관하게 고정 → public */
|
|
8
|
+
export declare const TOOLS_LIST_CACHE: {
|
|
9
|
+
readonly ttlMs: 300000;
|
|
10
|
+
readonly cacheScope: "public";
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* progressToken 있는 요청의 주기 progress 간격 (ms). 마지막 알림 후 이 시간 동안 알림이 없으면 보낸다.
|
|
14
|
+
* TS SDK 클라이언트 기본 타임아웃 60초 · 프록시 유휴 타임아웃 (보통 60초) 보다 충분히 짧게
|
|
15
|
+
*/
|
|
16
|
+
export declare const PROGRESS_INTERVAL_MS = 15000;
|
|
17
|
+
export interface McpCoreOptions {
|
|
18
|
+
registry: Registry;
|
|
19
|
+
/** ctx.app 생성. 처음 필요할 때 한 번 호출 (실패하면 다음 요청에서 재시도) */
|
|
20
|
+
app?: () => MextApp | Promise<MextApp>;
|
|
21
|
+
/** 기본 execute */
|
|
22
|
+
tools?: ToolExposure;
|
|
23
|
+
/** serverInfo.name. 기본 "mext" */
|
|
24
|
+
name?: string;
|
|
25
|
+
/** serverInfo.version. 기본 mext 버전 */
|
|
26
|
+
version?: string;
|
|
27
|
+
/**
|
|
28
|
+
* 요청 주체. 기본 = HTTP transport 가 authInfo 로 넘긴 principal, 없으면 ANONYMOUS.
|
|
29
|
+
* stdio = localPrincipal()
|
|
30
|
+
*/
|
|
31
|
+
principal?: Principal | ((ctx: McpRequestContext) => Principal);
|
|
32
|
+
/**
|
|
33
|
+
* MRTR requestState 서명 키 (HMAC-SHA256, 32바이트 이상). requestState 는 2번째 이후 ctx.ask 에서만 쓴다.
|
|
34
|
+
* 기본 = 프로세스별 임의 키. 여러 인스턴스 · 재시작을 넘는 재시도에는 공유 키를 지정한다
|
|
35
|
+
*/
|
|
36
|
+
requestStateKey?: string | Uint8Array;
|
|
37
|
+
/** 주기 progress 간격 (ms). 기본 PROGRESS_INTERVAL_MS, 0 = 끔 */
|
|
38
|
+
progressInterval?: number;
|
|
39
|
+
/** loadInstrumentation 결과 (onCommandError · span) */
|
|
40
|
+
instrumentation?: Instrumentation;
|
|
41
|
+
/** onCommandError · span 의 transport. HTTP · stdio 어댑터가 지정 */
|
|
42
|
+
transport?: Transport;
|
|
43
|
+
/**
|
|
44
|
+
* _meta["dev.mext/manifestHash"] 값. 기본 = registry 명령어로 계산 (mext dev = 스캔 결과).
|
|
45
|
+
* mext start = manifest.json 의 manifestHash (같은 함수로 계산한 값)
|
|
46
|
+
*/
|
|
47
|
+
manifestHash?: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Registry → MCP 서버 factory (SDK McpServerFactory). 요청 (HTTP) · 연결 (stdio) 마다 새 McpServer.
|
|
51
|
+
* HTTP 의 createMcpHandler 와 stdio 의 serveStdio 가 같은 factory 를 쓴다.
|
|
52
|
+
*/
|
|
53
|
+
export declare function createMcpServerFactory(options: McpCoreOptions): McpServerFactory;
|
|
54
|
+
/** registry 명령어 메타데이터의 manifestHash (빌드 manifest 와 같은 계산) */
|
|
55
|
+
export declare function registryManifestHash(registry: Registry): string;
|
|
56
|
+
/** HTTP transport: 검증된 principal → SDK authInfo (토큰 원문은 담지 않는다) */
|
|
57
|
+
export declare function principalAuthInfo(principal: Principal): AuthInfo;
|
|
58
|
+
export declare function principalFromAuthInfo(authInfo: AuthInfo | undefined): Principal | undefined;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { CLIENT_CAPABILITIES_META_KEY, McpServer, } from "@modelcontextprotocol/server";
|
|
2
|
+
import { ANONYMOUS } from "../principal.js";
|
|
3
|
+
import { VERSION } from "../index.js";
|
|
4
|
+
import { describeCommand, manifestHash } from "../manifest.js";
|
|
5
|
+
import { MANIFEST_HASH_META_KEY } from "./constants.js";
|
|
6
|
+
import { callDispatcher, dispatcherTools } from "./dispatch.js";
|
|
7
|
+
import { createRequestStateSigner } from "./mrtr.js";
|
|
8
|
+
/** tools/list · server/discover 캐시 힌트. 도구 정의는 principal 과 무관하게 고정 → public */
|
|
9
|
+
export const TOOLS_LIST_CACHE = { ttlMs: 300_000, cacheScope: "public" };
|
|
10
|
+
/**
|
|
11
|
+
* progressToken 있는 요청의 주기 progress 간격 (ms). 마지막 알림 후 이 시간 동안 알림이 없으면 보낸다.
|
|
12
|
+
* TS SDK 클라이언트 기본 타임아웃 60초 · 프록시 유휴 타임아웃 (보통 60초) 보다 충분히 짧게
|
|
13
|
+
*/
|
|
14
|
+
export const PROGRESS_INTERVAL_MS = 15_000;
|
|
15
|
+
/**
|
|
16
|
+
* Registry → MCP 서버 factory (SDK McpServerFactory). 요청 (HTTP) · 연결 (stdio) 마다 새 McpServer.
|
|
17
|
+
* HTTP 의 createMcpHandler 와 stdio 의 serveStdio 가 같은 factory 를 쓴다.
|
|
18
|
+
*/
|
|
19
|
+
export function createMcpServerFactory(options) {
|
|
20
|
+
const tools = dispatcherTools(options.registry, options.tools ?? "execute");
|
|
21
|
+
const getApp = memoizeApp(options.app);
|
|
22
|
+
const serverInfo = { name: options.name ?? "mext", version: options.version ?? VERSION };
|
|
23
|
+
const signer = createRequestStateSigner(options.requestStateKey === undefined ? {} : { key: options.requestStateKey });
|
|
24
|
+
const meta = { [MANIFEST_HASH_META_KEY]: options.manifestHash ?? registryManifestHash(options.registry) };
|
|
25
|
+
return (requestContext) => {
|
|
26
|
+
const principal = resolvePrincipal(options.principal, requestContext);
|
|
27
|
+
const server = new McpServer(serverInfo, {
|
|
28
|
+
capabilities: { tools: { listChanged: false } },
|
|
29
|
+
cacheHints: { "tools/list": TOOLS_LIST_CACHE, "server/discover": TOOLS_LIST_CACHE },
|
|
30
|
+
});
|
|
31
|
+
withDiscoverMeta(server, meta);
|
|
32
|
+
for (const tool of tools) {
|
|
33
|
+
server.registerTool(tool.name, {
|
|
34
|
+
title: tool.annotations.title,
|
|
35
|
+
description: tool.description,
|
|
36
|
+
inputSchema: passthroughSchema(tool.inputSchema),
|
|
37
|
+
annotations: tool.annotations,
|
|
38
|
+
}, async (args, ctx) => withMeta(await runTool(options, tools, tool, args, ctx, principal, getApp, signer), meta));
|
|
39
|
+
}
|
|
40
|
+
return server;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** registry 명령어 메타데이터의 manifestHash (빌드 manifest 와 같은 계산) */
|
|
44
|
+
export function registryManifestHash(registry) {
|
|
45
|
+
return manifestHash([...registry.commands.values()].map(describeCommand));
|
|
46
|
+
}
|
|
47
|
+
/** 결과 _meta 에 mext 키 추가 (SDK 의 serverInfo 등 기존 _meta 유지) */
|
|
48
|
+
function withMeta(result, meta) {
|
|
49
|
+
return { ...result, _meta: { ...result._meta, ...meta } };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* server/discover 결과에 _meta 추가. SDK 의 discover handler 는 McpServer 생성 후 transport 진입점이
|
|
53
|
+
* (재)설치하고 매번 server._ondiscover() 를 부르므로 인스턴스 메서드를 감싼다 (공개 API 에 discover _meta 옵션 없음)
|
|
54
|
+
*/
|
|
55
|
+
function withDiscoverMeta(server, meta) {
|
|
56
|
+
const inner = server.server;
|
|
57
|
+
const original = inner._ondiscover;
|
|
58
|
+
if (typeof original !== "function")
|
|
59
|
+
return;
|
|
60
|
+
inner._ondiscover = function () {
|
|
61
|
+
return withMeta(original.call(inner), meta);
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async function runTool(options, tools, tool, args, ctx, principal, getApp, signer) {
|
|
65
|
+
const progress = progressReporter(ctx, options.progressInterval ?? PROGRESS_INTERVAL_MS);
|
|
66
|
+
try {
|
|
67
|
+
const app = await getApp();
|
|
68
|
+
const result = await callDispatcher(options.registry, tool, args, {
|
|
69
|
+
principal,
|
|
70
|
+
signal: ctx.mcpReq.signal,
|
|
71
|
+
...(progress ? { progress: progress.report } : {}),
|
|
72
|
+
app,
|
|
73
|
+
tools,
|
|
74
|
+
input: { ...inputRound(ctx), signer },
|
|
75
|
+
...(options.instrumentation ? { instrumentation: options.instrumentation, traceContext: traceCarrier(ctx) } : {}),
|
|
76
|
+
...(options.transport ? { transport: options.transport } : {}),
|
|
77
|
+
});
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
progress?.stop();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** 요청의 MRTR 입력: elicitation (form) 선언 · inputResponses · requestState (SDK 가 params 에서 분리) */
|
|
85
|
+
function inputRound(ctx) {
|
|
86
|
+
const envelope = ctx.mcpReq.envelope;
|
|
87
|
+
const capabilities = envelope?.[CLIENT_CAPABILITIES_META_KEY];
|
|
88
|
+
const state = ctx.mcpReq.requestState();
|
|
89
|
+
return {
|
|
90
|
+
elicitation: declaresFormElicitation(capabilities?.elicitation),
|
|
91
|
+
...(ctx.mcpReq.inputResponses ? { inputResponses: ctx.mcpReq.inputResponses } : {}),
|
|
92
|
+
...(typeof state === "string" ? { requestState: state } : {}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** D6: params._meta 의 W3C trace context (traceparent · tracestate · baggage). 등록된 propagator 가 해석 */
|
|
96
|
+
function traceCarrier(ctx) {
|
|
97
|
+
const meta = ctx.mcpReq._meta;
|
|
98
|
+
let carrier;
|
|
99
|
+
for (const key of ["traceparent", "tracestate", "baggage"]) {
|
|
100
|
+
const value = meta?.[key];
|
|
101
|
+
if (typeof value === "string")
|
|
102
|
+
(carrier ??= {})[key] = value;
|
|
103
|
+
}
|
|
104
|
+
return carrier;
|
|
105
|
+
}
|
|
106
|
+
/** elicitation.form 선언 또는 mode 없는 elicitation: {} (SDK 와 같은 해석). 2025 레거시 요청 = envelope 없음 → false */
|
|
107
|
+
function declaresFormElicitation(value) {
|
|
108
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
109
|
+
return false;
|
|
110
|
+
const modes = value;
|
|
111
|
+
return modes.form !== undefined || modes.url === undefined;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* ctx.progress → notifications/progress. 요청에 progressToken 이 있을 때만 (없으면 undefined = 무시).
|
|
115
|
+
* HTTP 는 progressToken 이 있는 요청을 SSE 로 응답하므로 알림이 스트림으로 간다.
|
|
116
|
+
* 주기 progress: 마지막 알림 후 intervalMs 동안 알림이 없으면 마지막 total · message 를 다시 보낸다 (keepalive).
|
|
117
|
+
* progress 값은 항상 증가 (사양 MUST): 증가하지 않는 값 · keepalive 는 직전 값에 아주 작은 값을 더한다.
|
|
118
|
+
*/
|
|
119
|
+
function progressReporter(ctx, intervalMs) {
|
|
120
|
+
const token = ctx.mcpReq._meta?.progressToken;
|
|
121
|
+
if (token === undefined)
|
|
122
|
+
return undefined;
|
|
123
|
+
let last;
|
|
124
|
+
let timer;
|
|
125
|
+
let stopped = false;
|
|
126
|
+
const schedule = () => {
|
|
127
|
+
if (timer !== undefined)
|
|
128
|
+
clearTimeout(timer);
|
|
129
|
+
if (intervalMs > 0 && !stopped)
|
|
130
|
+
timer = setTimeout(() => report({ ...(last ?? { progress: 0 }) }), intervalMs);
|
|
131
|
+
};
|
|
132
|
+
const report = (update) => {
|
|
133
|
+
if (stopped || ctx.mcpReq.signal.aborted)
|
|
134
|
+
return;
|
|
135
|
+
const progress = last === undefined || update.progress > last.progress ? update.progress : increase(last.progress);
|
|
136
|
+
last = { progress, ...(update.total !== undefined ? { total: update.total } : {}), ...(update.message !== undefined ? { message: update.message } : {}) };
|
|
137
|
+
const params = { progressToken: token, ...last };
|
|
138
|
+
void ctx.mcpReq.notify({ method: "notifications/progress", params }).catch(() => { });
|
|
139
|
+
schedule();
|
|
140
|
+
};
|
|
141
|
+
schedule();
|
|
142
|
+
return {
|
|
143
|
+
report,
|
|
144
|
+
stop() {
|
|
145
|
+
stopped = true;
|
|
146
|
+
if (timer !== undefined)
|
|
147
|
+
clearTimeout(timer);
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/** 직전 값보다 큰 가장 가까운 값 (표시에는 드러나지 않는 크기) */
|
|
152
|
+
function increase(value) {
|
|
153
|
+
return value + Math.max(Math.abs(value), 1) * 1e-9;
|
|
154
|
+
}
|
|
155
|
+
/* principal 은 SDK 의 pass-through authInfo 로 transport → factory 에 전달 */
|
|
156
|
+
const PRINCIPAL_KEY = "mext.principal";
|
|
157
|
+
/** HTTP transport: 검증된 principal → SDK authInfo (토큰 원문은 담지 않는다) */
|
|
158
|
+
export function principalAuthInfo(principal) {
|
|
159
|
+
return { token: "", clientId: principal.id, scopes: [...principal.scopes], extra: { [PRINCIPAL_KEY]: principal } };
|
|
160
|
+
}
|
|
161
|
+
export function principalFromAuthInfo(authInfo) {
|
|
162
|
+
const value = authInfo?.extra?.[PRINCIPAL_KEY];
|
|
163
|
+
return isPrincipal(value) ? value : undefined;
|
|
164
|
+
}
|
|
165
|
+
function resolvePrincipal(option, ctx) {
|
|
166
|
+
if (typeof option === "function")
|
|
167
|
+
return option(ctx);
|
|
168
|
+
if (option)
|
|
169
|
+
return option;
|
|
170
|
+
return principalFromAuthInfo(ctx.authInfo) ?? ANONYMOUS;
|
|
171
|
+
}
|
|
172
|
+
function isPrincipal(value) {
|
|
173
|
+
const p = value;
|
|
174
|
+
return typeof p?.id === "string" && typeof p.kind === "string" && Array.isArray(p.scopes);
|
|
175
|
+
}
|
|
176
|
+
function memoizeApp(factory) {
|
|
177
|
+
if (!factory)
|
|
178
|
+
return async () => ({});
|
|
179
|
+
let pending;
|
|
180
|
+
return () => {
|
|
181
|
+
pending ??= Promise.resolve()
|
|
182
|
+
.then(factory)
|
|
183
|
+
.catch((error) => {
|
|
184
|
+
pending = undefined;
|
|
185
|
+
throw error;
|
|
186
|
+
});
|
|
187
|
+
return pending;
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* 고정 JSON Schema 를 그대로 광고하고 검증은 하지 않는 Standard Schema.
|
|
192
|
+
* dispatcher 인자 검증은 callDispatcher 가 구조화 오류 (INVALID_ARGUMENT) 로 수행한다.
|
|
193
|
+
*/
|
|
194
|
+
function passthroughSchema(schema) {
|
|
195
|
+
return {
|
|
196
|
+
"~standard": {
|
|
197
|
+
version: 1,
|
|
198
|
+
vendor: "mext",
|
|
199
|
+
validate: (value) => ({ value: value }),
|
|
200
|
+
jsonSchema: { input: () => schema, output: () => schema },
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n CLIENT_CAPABILITIES_META_KEY,\n McpServer,\n type AuthInfo,\n type CallToolResult,\n type InputRequiredResult,\n type McpRequestContext,\n type McpServerFactory,\n type ServerContext,\n type StandardSchemaWithJSON,\n} from \"@modelcontextprotocol/server\"\nimport type { MextApp, ProgressUpdate } from \"../command.ts\"\nimport type { Instrumentation, Transport } from \"../instrumentation.ts\"\nimport { ANONYMOUS, type Principal } from \"../principal.ts\"\nimport type { JsonSchema, Registry } from \"../registry.ts\"\nimport { VERSION } from \"../index.ts\"\nimport { describeCommand, manifestHash } from \"../manifest.ts\"\nimport { MANIFEST_HASH_META_KEY } from \"./constants.ts\"\nimport { callDispatcher, dispatcherTools, type DispatcherTool, type ToolExposure } from \"./dispatch.ts\"\nimport { createRequestStateSigner, type InputRound, type RequestStateSigner } from \"./mrtr.ts\"\n\n/** tools/list · server/discover 캐시 힌트. 도구 정의는 principal 과 무관하게 고정 → public */\nexport const TOOLS_LIST_CACHE = { ttlMs: 300_000, cacheScope: \"public\" } as const\n\n/**\n * progressToken 있는 요청의 주기 progress 간격 (ms). 마지막 알림 후 이 시간 동안 알림이 없으면 보낸다.\n * TS SDK 클라이언트 기본 타임아웃 60초 · 프록시 유휴 타임아웃 (보통 60초) 보다 충분히 짧게\n */\nexport const PROGRESS_INTERVAL_MS = 15_000\n\n\nexport interface McpCoreOptions {\n registry: Registry\n /** ctx.app 생성. 처음 필요할 때 한 번 호출 (실패하면 다음 요청에서 재시도) */\n app?: () => MextApp | Promise<MextApp>\n /** 기본 execute */\n tools?: ToolExposure\n /** serverInfo.name. 기본 \"mext\" */\n name?: string\n /** serverInfo.version. 기본 mext 버전 */\n version?: string\n /**\n * 요청 주체. 기본 = HTTP transport 가 authInfo 로 넘긴 principal, 없으면 ANONYMOUS.\n * stdio = localPrincipal()\n */\n principal?: Principal | ((ctx: McpRequestContext) => Principal)\n /**\n * MRTR requestState 서명 키 (HMAC-SHA256, 32바이트 이상). requestState 는 2번째 이후 ctx.ask 에서만 쓴다.\n * 기본 = 프로세스별 임의 키. 여러 인스턴스 · 재시작을 넘는 재시도에는 공유 키를 지정한다\n */\n requestStateKey?: string | Uint8Array\n /** 주기 progress 간격 (ms). 기본 PROGRESS_INTERVAL_MS, 0 = 끔 */\n progressInterval?: number\n /** loadInstrumentation 결과 (onCommandError · span) */\n instrumentation?: Instrumentation\n /** onCommandError · span 의 transport. HTTP · stdio 어댑터가 지정 */\n transport?: Transport\n /**\n * _meta[\"dev.mext/manifestHash\"] 값. 기본 = registry 명령어로 계산 (mext dev = 스캔 결과).\n * mext start = manifest.json 의 manifestHash (같은 함수로 계산한 값)\n */\n manifestHash?: string\n}\n\n/**\n * Registry → MCP 서버 factory (SDK McpServerFactory). 요청 (HTTP) · 연결 (stdio) 마다 새 McpServer.\n * HTTP 의 createMcpHandler 와 stdio 의 serveStdio 가 같은 factory 를 쓴다.\n */\nexport function createMcpServerFactory(options: McpCoreOptions): McpServerFactory {\n const tools = dispatcherTools(options.registry, options.tools ?? \"execute\")\n const getApp = memoizeApp(options.app)\n const serverInfo = { name: options.name ?? \"mext\", version: options.version ?? VERSION }\n const signer = createRequestStateSigner(options.requestStateKey === undefined ? {} : { key: options.requestStateKey })\n const meta = { [MANIFEST_HASH_META_KEY]: options.manifestHash ?? registryManifestHash(options.registry) }\n return (requestContext) => {\n const principal = resolvePrincipal(options.principal, requestContext)\n const server = new McpServer(serverInfo, {\n capabilities: { tools: { listChanged: false } },\n cacheHints: { \"tools/list\": TOOLS_LIST_CACHE, \"server/discover\": TOOLS_LIST_CACHE },\n })\n withDiscoverMeta(server, meta)\n for (const tool of tools) {\n server.registerTool(\n tool.name,\n {\n title: tool.annotations.title,\n description: tool.description,\n inputSchema: passthroughSchema(tool.inputSchema),\n annotations: tool.annotations,\n },\n async (args, ctx) => withMeta(await runTool(options, tools, tool, args, ctx, principal, getApp, signer), meta),\n )\n }\n return server\n }\n}\n\n/** registry 명령어 메타데이터의 manifestHash (빌드 manifest 와 같은 계산) */\nexport function registryManifestHash(registry: Registry): string {\n return manifestHash([...registry.commands.values()].map(describeCommand))\n}\n\n/** 결과 _meta 에 mext 키 추가 (SDK 의 serverInfo 등 기존 _meta 유지) */\nfunction withMeta<T extends { _meta?: Record<string, unknown> }>(result: T, meta: Record<string, unknown>): T {\n return { ...result, _meta: { ...result._meta, ...meta } }\n}\n\n/**\n * server/discover 결과에 _meta 추가. SDK 의 discover handler 는 McpServer 생성 후 transport 진입점이\n * (재)설치하고 매번 server._ondiscover() 를 부르므로 인스턴스 메서드를 감싼다 (공개 API 에 discover _meta 옵션 없음)\n */\nfunction withDiscoverMeta(server: McpServer, meta: Record<string, unknown>): void {\n const inner = server.server as unknown as { _ondiscover?: () => Record<string, unknown> }\n const original = inner._ondiscover\n if (typeof original !== \"function\") return\n inner._ondiscover = function (this: unknown) {\n return withMeta(original.call(inner) as { _meta?: Record<string, unknown> }, meta)\n }\n}\n\nasync function runTool(\n options: McpCoreOptions,\n tools: readonly DispatcherTool[],\n tool: DispatcherTool,\n args: unknown,\n ctx: ServerContext,\n principal: Principal,\n getApp: () => Promise<MextApp>,\n signer: RequestStateSigner,\n): Promise<CallToolResult | InputRequiredResult> {\n const progress = progressReporter(ctx, options.progressInterval ?? PROGRESS_INTERVAL_MS)\n try {\n const app = await getApp()\n const result = await callDispatcher(options.registry, tool, args, {\n principal,\n signal: ctx.mcpReq.signal,\n ...(progress ? { progress: progress.report } : {}),\n app,\n tools,\n input: { ...inputRound(ctx), signer },\n ...(options.instrumentation ? { instrumentation: options.instrumentation, traceContext: traceCarrier(ctx) } : {}),\n ...(options.transport ? { transport: options.transport } : {}),\n })\n return result as CallToolResult | InputRequiredResult\n } finally {\n progress?.stop()\n }\n}\n\n/** 요청의 MRTR 입력: elicitation (form) 선언 · inputResponses · requestState (SDK 가 params 에서 분리) */\nfunction inputRound(ctx: ServerContext): InputRound {\n const envelope = ctx.mcpReq.envelope as Record<string, unknown> | undefined\n const capabilities = envelope?.[CLIENT_CAPABILITIES_META_KEY] as { elicitation?: unknown } | undefined\n const state = ctx.mcpReq.requestState()\n return {\n elicitation: declaresFormElicitation(capabilities?.elicitation),\n ...(ctx.mcpReq.inputResponses ? { inputResponses: ctx.mcpReq.inputResponses } : {}),\n ...(typeof state === \"string\" ? { requestState: state } : {}),\n }\n}\n\n/** D6: params._meta 의 W3C trace context (traceparent · tracestate · baggage). 등록된 propagator 가 해석 */\nfunction traceCarrier(ctx: ServerContext): Record<string, string> | undefined {\n const meta = ctx.mcpReq._meta as Record<string, unknown> | undefined\n let carrier: Record<string, string> | undefined\n for (const key of [\"traceparent\", \"tracestate\", \"baggage\"]) {\n const value = meta?.[key]\n if (typeof value === \"string\") (carrier ??= {})[key] = value\n }\n return carrier\n}\n\n/** elicitation.form 선언 또는 mode 없는 elicitation: {} (SDK 와 같은 해석). 2025 레거시 요청 = envelope 없음 → false */\nfunction declaresFormElicitation(value: unknown): boolean {\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) return false\n const modes = value as { form?: unknown; url?: unknown }\n return modes.form !== undefined || modes.url === undefined\n}\n\n/**\n * ctx.progress → notifications/progress. 요청에 progressToken 이 있을 때만 (없으면 undefined = 무시).\n * HTTP 는 progressToken 이 있는 요청을 SSE 로 응답하므로 알림이 스트림으로 간다.\n * 주기 progress: 마지막 알림 후 intervalMs 동안 알림이 없으면 마지막 total · message 를 다시 보낸다 (keepalive).\n * progress 값은 항상 증가 (사양 MUST): 증가하지 않는 값 · keepalive 는 직전 값에 아주 작은 값을 더한다.\n */\nfunction progressReporter(\n ctx: ServerContext,\n intervalMs: number,\n): { report: (update: ProgressUpdate) => void; stop: () => void } | undefined {\n const token = ctx.mcpReq._meta?.progressToken\n if (token === undefined) return undefined\n let last: ProgressUpdate | undefined\n let timer: ReturnType<typeof setTimeout> | undefined\n let stopped = false\n const schedule = () => {\n if (timer !== undefined) clearTimeout(timer)\n if (intervalMs > 0 && !stopped) timer = setTimeout(() => report({ ...(last ?? { progress: 0 }) }), intervalMs)\n }\n const report = (update: ProgressUpdate) => {\n if (stopped || ctx.mcpReq.signal.aborted) return\n const progress = last === undefined || update.progress > last.progress ? update.progress : increase(last.progress)\n last = { progress, ...(update.total !== undefined ? { total: update.total } : {}), ...(update.message !== undefined ? { message: update.message } : {}) }\n const params: Record<string, unknown> = { progressToken: token, ...last }\n void ctx.mcpReq.notify({ method: \"notifications/progress\", params }).catch(() => {})\n schedule()\n }\n schedule()\n return {\n report,\n stop() {\n stopped = true\n if (timer !== undefined) clearTimeout(timer)\n },\n }\n}\n\n/** 직전 값보다 큰 가장 가까운 값 (표시에는 드러나지 않는 크기) */\nfunction increase(value: number): number {\n return value + Math.max(Math.abs(value), 1) * 1e-9\n}\n\n/* principal 은 SDK 의 pass-through authInfo 로 transport → factory 에 전달 */\n\nconst PRINCIPAL_KEY = \"mext.principal\"\n\n/** HTTP transport: 검증된 principal → SDK authInfo (토큰 원문은 담지 않는다) */\nexport function principalAuthInfo(principal: Principal): AuthInfo {\n return { token: \"\", clientId: principal.id, scopes: [...principal.scopes], extra: { [PRINCIPAL_KEY]: principal } }\n}\n\nexport function principalFromAuthInfo(authInfo: AuthInfo | undefined): Principal | undefined {\n const value = authInfo?.extra?.[PRINCIPAL_KEY]\n return isPrincipal(value) ? value : undefined\n}\n\nfunction resolvePrincipal(option: McpCoreOptions[\"principal\"], ctx: McpRequestContext): Principal {\n if (typeof option === \"function\") return option(ctx)\n if (option) return option\n return principalFromAuthInfo(ctx.authInfo) ?? ANONYMOUS\n}\n\nfunction isPrincipal(value: unknown): value is Principal {\n const p = value as Partial<Principal> | undefined\n return typeof p?.id === \"string\" && typeof p.kind === \"string\" && Array.isArray(p.scopes)\n}\n\nfunction memoizeApp(factory: McpCoreOptions[\"app\"]): () => Promise<MextApp> {\n if (!factory) return async () => ({}) as MextApp\n let pending: Promise<MextApp> | undefined\n return () => {\n pending ??= Promise.resolve()\n .then(factory)\n .catch((error: unknown) => {\n pending = undefined\n throw error\n })\n return pending\n }\n}\n\n/**\n * 고정 JSON Schema 를 그대로 광고하고 검증은 하지 않는 Standard Schema.\n * dispatcher 인자 검증은 callDispatcher 가 구조화 오류 (INVALID_ARGUMENT) 로 수행한다.\n */\nfunction passthroughSchema(schema: JsonSchema): StandardSchemaWithJSON<Record<string, unknown>> {\n return {\n \"~standard\": {\n version: 1,\n vendor: \"mext\",\n validate: (value: unknown) => ({ value: value as Record<string, unknown> }),\n jsonSchema: { input: () => schema, output: () => schema },\n },\n }\n}\n"]}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Readable } from "node:stream";
|
|
2
|
+
import { Writable } from "node:stream";
|
|
3
|
+
import type { MextApp } from "../command.ts";
|
|
4
|
+
import { type Principal } from "../principal.ts";
|
|
5
|
+
import type { Registry } from "../registry.ts";
|
|
6
|
+
import type { Instrumentation } from "../instrumentation.ts";
|
|
7
|
+
import type { ToolExposure } from "./dispatch.ts";
|
|
8
|
+
import { type McpCoreOptions } from "./server.ts";
|
|
9
|
+
export interface McpStdioOptions {
|
|
10
|
+
registry: Registry;
|
|
11
|
+
app?: () => MextApp | Promise<MextApp>;
|
|
12
|
+
tools?: ToolExposure;
|
|
13
|
+
/** serverInfo.name. 기본 "mext" */
|
|
14
|
+
name?: string;
|
|
15
|
+
/** 연결 주체 (로컬 모드). 기본 localPrincipal() */
|
|
16
|
+
principal?: Principal;
|
|
17
|
+
/** 기본 process.stdin */
|
|
18
|
+
stdin?: Readable;
|
|
19
|
+
/**
|
|
20
|
+
* MCP 메시지 출력. 기본 process.stdout.
|
|
21
|
+
* D4: process.stdout 을 쓰는 경우 redirectStdout() 의 wire 를 넘겨 핸들러 출력이 메시지와 섞이지 않게 한다
|
|
22
|
+
*/
|
|
23
|
+
stdout?: Writable;
|
|
24
|
+
/** 응답에 영향 없는 오류 보고 (SDK 내부 오류 · 깨진 메시지) */
|
|
25
|
+
onerror?: (error: Error) => void;
|
|
26
|
+
/** MRTR requestState 서명 키 (createMcpServerFactory 와 같음) */
|
|
27
|
+
requestStateKey?: McpCoreOptions["requestStateKey"];
|
|
28
|
+
/** 주기 progress 간격 (ms). 0 = 끔 */
|
|
29
|
+
progressInterval?: number;
|
|
30
|
+
/** loadInstrumentation 결과 (onCommandError · span, transport = "stdio") */
|
|
31
|
+
instrumentation?: Instrumentation;
|
|
32
|
+
/** _meta["dev.mext/manifestHash"]. McpCoreOptions.manifestHash (기본 = registry 로 계산) */
|
|
33
|
+
manifestHash?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface McpStdioServer {
|
|
36
|
+
/** stdin EOF · stdout 오류 · close() 로 연결이 끝나면 resolve */
|
|
37
|
+
closed: Promise<void>;
|
|
38
|
+
close(): Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* MCP stdio (2026-07-28 · 2025 레거시 모두 SDK serveStdio). 줄 단위 JSON-RPC, 연결 하나 = McpServer 하나.
|
|
42
|
+
* notifications/cancelled → ctx.signal abort (SDK). stdin EOF = 연결 종료, 진행 중 요청은 abort 되고 응답하지 않는다.
|
|
43
|
+
* 헤더 검증 · Origin · 바인딩 · Authorization 은 해당 없음. 주체 = 로컬 (kind local).
|
|
44
|
+
*/
|
|
45
|
+
export declare function serveMcpStdio(options: McpStdioOptions): McpStdioServer;
|
|
46
|
+
export interface StdoutRedirect {
|
|
47
|
+
/** 원래 process.stdout 으로 쓰는 스트림. MCP transport 전용 */
|
|
48
|
+
wire: Writable;
|
|
49
|
+
/** process.stdout.write 원복 */
|
|
50
|
+
restore(): void;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* D4: stdio 모드에서 transport 외의 stdout 쓰기 (console.log · process.stdout.write) 를 stderr 로 돌린다.
|
|
54
|
+
* 반환된 wire 만 실제 stdout 에 쓴다. 명령어 모듈 import 전에 호출해야 top-level 출력도 막는다.
|
|
55
|
+
* 자식 프로세스 · fd 1 직접 쓰기 (fs.writeSync(1)) 는 막지 못한다.
|
|
56
|
+
*/
|
|
57
|
+
export declare function redirectStdout(): StdoutRedirect;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Writable } from "node:stream";
|
|
2
|
+
import { serveStdio, StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
3
|
+
import { localPrincipal } from "../principal.js";
|
|
4
|
+
import { createMcpServerFactory } from "./server.js";
|
|
5
|
+
/**
|
|
6
|
+
* MCP stdio (2026-07-28 · 2025 레거시 모두 SDK serveStdio). 줄 단위 JSON-RPC, 연결 하나 = McpServer 하나.
|
|
7
|
+
* notifications/cancelled → ctx.signal abort (SDK). stdin EOF = 연결 종료, 진행 중 요청은 abort 되고 응답하지 않는다.
|
|
8
|
+
* 헤더 검증 · Origin · 바인딩 · Authorization 은 해당 없음. 주체 = 로컬 (kind local).
|
|
9
|
+
*/
|
|
10
|
+
export function serveMcpStdio(options) {
|
|
11
|
+
const factory = createMcpServerFactory({
|
|
12
|
+
registry: options.registry,
|
|
13
|
+
principal: options.principal ?? localPrincipal(),
|
|
14
|
+
...(options.app ? { app: options.app } : {}),
|
|
15
|
+
...(options.tools ? { tools: options.tools } : {}),
|
|
16
|
+
...(options.name ? { name: options.name } : {}),
|
|
17
|
+
...(options.requestStateKey !== undefined ? { requestStateKey: options.requestStateKey } : {}),
|
|
18
|
+
...(options.progressInterval !== undefined ? { progressInterval: options.progressInterval } : {}),
|
|
19
|
+
...(options.instrumentation ? { instrumentation: options.instrumentation } : {}),
|
|
20
|
+
...(options.manifestHash !== undefined ? { manifestHash: options.manifestHash } : {}),
|
|
21
|
+
transport: "stdio",
|
|
22
|
+
});
|
|
23
|
+
const transport = new StdioServerTransport(options.stdin, options.stdout);
|
|
24
|
+
let resolveClosed;
|
|
25
|
+
const closed = new Promise((resolve) => {
|
|
26
|
+
resolveClosed = resolve;
|
|
27
|
+
});
|
|
28
|
+
const handle = serveStdio(factory, options.onerror ? { transport, onerror: options.onerror } : { transport });
|
|
29
|
+
// serveStdio 가 onclose 를 동기적으로 설정한 뒤 감싼다 (stdin 이벤트는 비동기)
|
|
30
|
+
const sdkOnClose = transport.onclose;
|
|
31
|
+
transport.onclose = () => {
|
|
32
|
+
try {
|
|
33
|
+
sdkOnClose?.();
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
resolveClosed();
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
closed,
|
|
41
|
+
async close() {
|
|
42
|
+
await handle.close();
|
|
43
|
+
resolveClosed();
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* D4: stdio 모드에서 transport 외의 stdout 쓰기 (console.log · process.stdout.write) 를 stderr 로 돌린다.
|
|
49
|
+
* 반환된 wire 만 실제 stdout 에 쓴다. 명령어 모듈 import 전에 호출해야 top-level 출력도 막는다.
|
|
50
|
+
* 자식 프로세스 · fd 1 직접 쓰기 (fs.writeSync(1)) 는 막지 못한다.
|
|
51
|
+
*/
|
|
52
|
+
export function redirectStdout() {
|
|
53
|
+
const stdout = process.stdout;
|
|
54
|
+
const original = stdout.write;
|
|
55
|
+
const toStderr = (...args) => process.stderr.write.apply(process.stderr, args);
|
|
56
|
+
stdout.write = toStderr;
|
|
57
|
+
// EPIPE 등은 wire 의 write 콜백으로 transport 에 전달된다. process.stdout 의 미처리 error 로 죽지 않게
|
|
58
|
+
const ignore = () => { };
|
|
59
|
+
stdout.on("error", ignore);
|
|
60
|
+
const wire = new Writable({
|
|
61
|
+
write(chunk, _encoding, callback) {
|
|
62
|
+
const write = original;
|
|
63
|
+
write.call(stdout, chunk, (error) => callback(error ?? null));
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
let restored = false;
|
|
67
|
+
return {
|
|
68
|
+
wire,
|
|
69
|
+
restore() {
|
|
70
|
+
if (restored)
|
|
71
|
+
return;
|
|
72
|
+
restored = true;
|
|
73
|
+
if (stdout.write === toStderr)
|
|
74
|
+
stdout.write = original;
|
|
75
|
+
stdout.off("error", ignore);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=stdio.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stdio.js","sourceRoot":"","sources":["../../src/mcp/stdio.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Readable } from \"node:stream\"\nimport { Writable } from \"node:stream\"\nimport { serveStdio, StdioServerTransport } from \"@modelcontextprotocol/server/stdio\"\nimport type { MextApp } from \"../command.ts\"\nimport { localPrincipal, type Principal } from \"../principal.ts\"\nimport type { Registry } from \"../registry.ts\"\nimport type { Instrumentation } from \"../instrumentation.ts\"\nimport type { ToolExposure } from \"./dispatch.ts\"\nimport { createMcpServerFactory, type McpCoreOptions } from \"./server.ts\"\n\nexport interface McpStdioOptions {\n registry: Registry\n app?: () => MextApp | Promise<MextApp>\n tools?: ToolExposure\n /** serverInfo.name. 기본 \"mext\" */\n name?: string\n /** 연결 주체 (로컬 모드). 기본 localPrincipal() */\n principal?: Principal\n /** 기본 process.stdin */\n stdin?: Readable\n /**\n * MCP 메시지 출력. 기본 process.stdout.\n * D4: process.stdout 을 쓰는 경우 redirectStdout() 의 wire 를 넘겨 핸들러 출력이 메시지와 섞이지 않게 한다\n */\n stdout?: Writable\n /** 응답에 영향 없는 오류 보고 (SDK 내부 오류 · 깨진 메시지) */\n onerror?: (error: Error) => void\n /** MRTR requestState 서명 키 (createMcpServerFactory 와 같음) */\n requestStateKey?: McpCoreOptions[\"requestStateKey\"]\n /** 주기 progress 간격 (ms). 0 = 끔 */\n progressInterval?: number\n /** loadInstrumentation 결과 (onCommandError · span, transport = \"stdio\") */\n instrumentation?: Instrumentation\n /** _meta[\"dev.mext/manifestHash\"]. McpCoreOptions.manifestHash (기본 = registry 로 계산) */\n manifestHash?: string\n}\n\nexport interface McpStdioServer {\n /** stdin EOF · stdout 오류 · close() 로 연결이 끝나면 resolve */\n closed: Promise<void>\n close(): Promise<void>\n}\n\n/**\n * MCP stdio (2026-07-28 · 2025 레거시 모두 SDK serveStdio). 줄 단위 JSON-RPC, 연결 하나 = McpServer 하나.\n * notifications/cancelled → ctx.signal abort (SDK). stdin EOF = 연결 종료, 진행 중 요청은 abort 되고 응답하지 않는다.\n * 헤더 검증 · Origin · 바인딩 · Authorization 은 해당 없음. 주체 = 로컬 (kind local).\n */\nexport function serveMcpStdio(options: McpStdioOptions): McpStdioServer {\n const factory = createMcpServerFactory({\n registry: options.registry,\n principal: options.principal ?? localPrincipal(),\n ...(options.app ? { app: options.app } : {}),\n ...(options.tools ? { tools: options.tools } : {}),\n ...(options.name ? { name: options.name } : {}),\n ...(options.requestStateKey !== undefined ? { requestStateKey: options.requestStateKey } : {}),\n ...(options.progressInterval !== undefined ? { progressInterval: options.progressInterval } : {}),\n ...(options.instrumentation ? { instrumentation: options.instrumentation } : {}),\n ...(options.manifestHash !== undefined ? { manifestHash: options.manifestHash } : {}),\n transport: \"stdio\",\n })\n const transport = new StdioServerTransport(options.stdin, options.stdout)\n let resolveClosed!: () => void\n const closed = new Promise<void>((resolve) => {\n resolveClosed = resolve\n })\n const handle = serveStdio(factory, options.onerror ? { transport, onerror: options.onerror } : { transport })\n // serveStdio 가 onclose 를 동기적으로 설정한 뒤 감싼다 (stdin 이벤트는 비동기)\n const sdkOnClose = transport.onclose\n transport.onclose = () => {\n try {\n sdkOnClose?.()\n } finally {\n resolveClosed()\n }\n }\n return {\n closed,\n async close() {\n await handle.close()\n resolveClosed()\n },\n }\n}\n\nexport interface StdoutRedirect {\n /** 원래 process.stdout 으로 쓰는 스트림. MCP transport 전용 */\n wire: Writable\n /** process.stdout.write 원복 */\n restore(): void\n}\n\n/**\n * D4: stdio 모드에서 transport 외의 stdout 쓰기 (console.log · process.stdout.write) 를 stderr 로 돌린다.\n * 반환된 wire 만 실제 stdout 에 쓴다. 명령어 모듈 import 전에 호출해야 top-level 출력도 막는다.\n * 자식 프로세스 · fd 1 직접 쓰기 (fs.writeSync(1)) 는 막지 못한다.\n */\nexport function redirectStdout(): StdoutRedirect {\n const stdout = process.stdout\n const original = stdout.write\n const toStderr = (...args: unknown[]): boolean =>\n (process.stderr.write as (...a: unknown[]) => boolean).apply(process.stderr, args)\n stdout.write = toStderr as typeof stdout.write\n // EPIPE 등은 wire 의 write 콜백으로 transport 에 전달된다. process.stdout 의 미처리 error 로 죽지 않게\n const ignore = (): void => {}\n stdout.on(\"error\", ignore)\n const wire = new Writable({\n write(chunk: Buffer | string, _encoding, callback) {\n const write = original as (chunk: Buffer | string, done: (error?: Error | null) => void) => boolean\n write.call(stdout, chunk, (error) => callback(error ?? null))\n },\n })\n let restored = false\n return {\n wire,\n restore() {\n if (restored) return\n restored = true\n if (stdout.write === (toStderr as typeof stdout.write)) stdout.write = original\n stdout.off(\"error\", ignore)\n },\n }\n}\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** 요청 주체. 서버가 요청마다 생성 (HTTP · CLI 원격), 로컬 · stdio = kind local */
|
|
2
|
+
export interface Principal {
|
|
3
|
+
/** "local:obov" | "user_123" | "anonymous" */
|
|
4
|
+
id: string;
|
|
5
|
+
kind: "local" | "token" | "anonymous";
|
|
6
|
+
/** "*" = 전체 */
|
|
7
|
+
scopes: string[];
|
|
8
|
+
/** 검증된 토큰 내용. 토큰 원문은 포함하지 않는다 */
|
|
9
|
+
claims?: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
/** 인증 없는 주체. scopes 없음 → readOnly 명령어만 */
|
|
12
|
+
export declare const ANONYMOUS: Principal;
|
|
13
|
+
/** 로컬 · stdio 실행의 기본 주체: { id: "local:<OS 사용자>", kind: "local", scopes: ["*"] } */
|
|
14
|
+
export declare function localPrincipal(user?: string): Principal;
|
|
15
|
+
/** "*" = 전체 */
|
|
16
|
+
export declare function hasScope(principal: Principal, scope: string): boolean;
|