ctxr-cli 0.29.0 → 0.31.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/README.md +43 -0
- package/dist/commands/land.d.ts.map +1 -1
- package/dist/commands/land.js +3 -18
- package/dist/commands/land.js.map +1 -1
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +10 -2
- package/dist/commands/serve.js.map +1 -1
- package/dist/config/defaults.d.ts +2 -0
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +2 -0
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/schema.d.ts +39 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +57 -8
- package/dist/config/schema.js.map +1 -1
- package/dist/core/browse/approve.d.ts +3 -0
- package/dist/core/browse/approve.d.ts.map +1 -1
- package/dist/core/browse/approve.js.map +1 -1
- package/dist/core/browse/page-url.d.ts +15 -0
- package/dist/core/browse/page-url.d.ts.map +1 -1
- package/dist/core/browse/page-url.js +8 -1
- package/dist/core/browse/page-url.js.map +1 -1
- package/dist/core/forge/credentials.d.ts +12 -1
- package/dist/core/forge/credentials.d.ts.map +1 -1
- package/dist/core/forge/credentials.js +25 -1
- package/dist/core/forge/credentials.js.map +1 -1
- package/dist/core/forge/github.d.ts +9 -0
- package/dist/core/forge/github.d.ts.map +1 -1
- package/dist/core/forge/github.js +6 -0
- package/dist/core/forge/github.js.map +1 -1
- package/dist/core/mcp/drafts.d.ts +30 -0
- package/dist/core/mcp/drafts.d.ts.map +1 -0
- package/dist/core/mcp/drafts.js +52 -0
- package/dist/core/mcp/drafts.js.map +1 -0
- package/dist/core/mcp/http.d.ts +40 -0
- package/dist/core/mcp/http.d.ts.map +1 -0
- package/dist/core/mcp/http.js +90 -0
- package/dist/core/mcp/http.js.map +1 -0
- package/dist/core/mcp/jsonrpc.d.ts +73 -0
- package/dist/core/mcp/jsonrpc.d.ts.map +1 -0
- package/dist/core/mcp/jsonrpc.js +149 -0
- package/dist/core/mcp/jsonrpc.js.map +1 -0
- package/dist/core/mcp/members.d.ts +11 -0
- package/dist/core/mcp/members.d.ts.map +1 -0
- package/dist/core/mcp/members.js +21 -0
- package/dist/core/mcp/members.js.map +1 -0
- package/dist/core/mcp/oauth.d.ts +99 -0
- package/dist/core/mcp/oauth.d.ts.map +1 -0
- package/dist/core/mcp/oauth.js +454 -0
- package/dist/core/mcp/oauth.js.map +1 -0
- package/dist/core/mcp/pages.d.ts +3 -0
- package/dist/core/mcp/pages.d.ts.map +1 -0
- package/dist/core/mcp/pages.js +41 -0
- package/dist/core/mcp/pages.js.map +1 -0
- package/dist/core/mcp/ripgrep.d.ts +48 -0
- package/dist/core/mcp/ripgrep.d.ts.map +1 -0
- package/dist/core/mcp/ripgrep.js +98 -0
- package/dist/core/mcp/ripgrep.js.map +1 -0
- package/dist/core/mcp/rwlock.d.ts +16 -0
- package/dist/core/mcp/rwlock.d.ts.map +1 -0
- package/dist/core/mcp/rwlock.js +59 -0
- package/dist/core/mcp/rwlock.js.map +1 -0
- package/dist/core/mcp/sessions.d.ts +84 -0
- package/dist/core/mcp/sessions.d.ts.map +1 -0
- package/dist/core/mcp/sessions.js +154 -0
- package/dist/core/mcp/sessions.js.map +1 -0
- package/dist/core/mcp/surface.d.ts +42 -0
- package/dist/core/mcp/surface.d.ts.map +1 -0
- package/dist/core/mcp/surface.js +141 -0
- package/dist/core/mcp/surface.js.map +1 -0
- package/dist/core/mcp/sync.d.ts +91 -0
- package/dist/core/mcp/sync.d.ts.map +1 -0
- package/dist/core/mcp/sync.js +218 -0
- package/dist/core/mcp/sync.js.map +1 -0
- package/dist/core/mcp/tools/capture.d.ts +64 -0
- package/dist/core/mcp/tools/capture.d.ts.map +1 -0
- package/dist/core/mcp/tools/capture.js +182 -0
- package/dist/core/mcp/tools/capture.js.map +1 -0
- package/dist/core/mcp/tools/edit.d.ts +79 -0
- package/dist/core/mcp/tools/edit.d.ts.map +1 -0
- package/dist/core/mcp/tools/edit.js +212 -0
- package/dist/core/mcp/tools/edit.js.map +1 -0
- package/dist/core/mcp/tools/files.d.ts +163 -0
- package/dist/core/mcp/tools/files.d.ts.map +1 -0
- package/dist/core/mcp/tools/files.js +265 -0
- package/dist/core/mcp/tools/files.js.map +1 -0
- package/dist/core/mcp/tools/find.d.ts +82 -0
- package/dist/core/mcp/tools/find.d.ts.map +1 -0
- package/dist/core/mcp/tools/find.js +136 -0
- package/dist/core/mcp/tools/find.js.map +1 -0
- package/dist/core/mcp/tools/gather.d.ts +88 -0
- package/dist/core/mcp/tools/gather.d.ts.map +1 -0
- package/dist/core/mcp/tools/gather.js +123 -0
- package/dist/core/mcp/tools/gather.js.map +1 -0
- package/dist/core/mcp/tools/index.d.ts +21 -0
- package/dist/core/mcp/tools/index.d.ts.map +1 -0
- package/dist/core/mcp/tools/index.js +129 -0
- package/dist/core/mcp/tools/index.js.map +1 -0
- package/dist/core/skills.d.ts +18 -2
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +51 -5
- package/dist/core/skills.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/templates/mcp/Edit.md +7 -0
- package/templates/mcp/Glob.md +5 -0
- package/templates/mcp/Grep.md +10 -0
- package/templates/mcp/Read.md +7 -0
- package/templates/mcp/Write.md +7 -0
- package/templates/mcp/capture.md +9 -0
- package/templates/mcp/consent.html +40 -0
- package/templates/mcp/find.md +9 -0
- package/templates/mcp/gather.md +5 -0
- package/templates/mcp/server-instructions.md +17 -0
- package/templates/skills/ctxr-circleback/sweep.mjs +229 -0
- package/templates/skills/ctxr-circleback.md +70 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { dispatch, parseErrorResponse } from './jsonrpc.js';
|
|
2
|
+
/**
|
|
3
|
+
* serve-the-store-over-mcp design.md D2/D10: the Streamable HTTP transport,
|
|
4
|
+
* reduced to what a stateless, stream-free server needs. `POST /mcp` carries
|
|
5
|
+
* JSON-RPC in and plain JSON out (202 when nothing needs an answer); `GET`
|
|
6
|
+
* and `DELETE` answer 405, which the transport permits for a server that
|
|
7
|
+
* opens no stream and issues no session.
|
|
8
|
+
*/
|
|
9
|
+
export const MCP_PATH = '/mcp';
|
|
10
|
+
/** Generous for a JSON-RPC message, small enough that a hostile body cannot exhaust memory. */
|
|
11
|
+
export const MAX_MCP_BODY_BYTES = 1024 * 1024;
|
|
12
|
+
export class BodyTooLargeError extends Error {
|
|
13
|
+
limit;
|
|
14
|
+
constructor(limit) {
|
|
15
|
+
super(`request body exceeds ${limit} bytes`);
|
|
16
|
+
this.limit = limit;
|
|
17
|
+
this.name = 'BodyTooLargeError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export async function readBody(req, limit) {
|
|
21
|
+
const chunks = [];
|
|
22
|
+
let total = 0;
|
|
23
|
+
for await (const chunk of req) {
|
|
24
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
25
|
+
total += buffer.length;
|
|
26
|
+
if (total > limit)
|
|
27
|
+
throw new BodyTooLargeError(limit);
|
|
28
|
+
chunks.push(buffer);
|
|
29
|
+
}
|
|
30
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
31
|
+
}
|
|
32
|
+
export function sendJson(res, status, body, headers = {}) {
|
|
33
|
+
const buffer = Buffer.from(JSON.stringify(body), 'utf8');
|
|
34
|
+
res.writeHead(status, { 'Content-Type': 'application/json', 'Content-Length': buffer.length, 'Cache-Control': 'no-store', ...headers });
|
|
35
|
+
res.end(buffer);
|
|
36
|
+
}
|
|
37
|
+
export function sendText(res, status, text, headers = {}) {
|
|
38
|
+
const buffer = Buffer.from(text, 'utf8');
|
|
39
|
+
res.writeHead(status, { 'Content-Type': 'text/plain; charset=utf-8', 'Content-Length': buffer.length, ...headers });
|
|
40
|
+
res.end(buffer);
|
|
41
|
+
}
|
|
42
|
+
/** Handles `/mcp`; returns false for any other path so the caller can route it. */
|
|
43
|
+
export async function handleMcpRequest(options, req, res, pathname) {
|
|
44
|
+
if (pathname !== MCP_PATH)
|
|
45
|
+
return false;
|
|
46
|
+
const origin = req.headers.origin;
|
|
47
|
+
if (origin !== undefined && !options.originAllowed(origin)) {
|
|
48
|
+
sendText(res, 403, 'origin not allowed\n');
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
const method = req.method ?? 'GET';
|
|
52
|
+
if (method !== 'POST') {
|
|
53
|
+
sendText(res, 405, 'method not allowed: this server opens no stream; send JSON-RPC by POST\n', { Allow: 'POST' });
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
const auth = await options.authenticate(req);
|
|
57
|
+
if (auth.kind === 'refused') {
|
|
58
|
+
sendJson(res, auth.status, auth.body, auth.headers);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
let text;
|
|
62
|
+
try {
|
|
63
|
+
text = await readBody(req, MAX_MCP_BODY_BYTES);
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
if (err instanceof BodyTooLargeError) {
|
|
67
|
+
sendText(res, 413, `${err.message}\n`);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
let body;
|
|
73
|
+
try {
|
|
74
|
+
body = JSON.parse(text);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
sendJson(res, 400, parseErrorResponse());
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
options.onMessage?.(auth.principal, body);
|
|
81
|
+
const answer = await dispatch(body, options.contextFor(auth.principal));
|
|
82
|
+
if (answer === null) {
|
|
83
|
+
res.writeHead(202, { 'Content-Length': 0 });
|
|
84
|
+
res.end();
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
sendJson(res, 200, answer);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../../src/core/mcp/http.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* serve-the-store-over-mcp design.md D2: the MCP subset the remote surface
|
|
3
|
+
* speaks, hand-rolled rather than taken from the SDK. Four methods, every
|
|
4
|
+
* notification accepted and ignored, everything else refused in protocol.
|
|
5
|
+
* Transport-free: `http.ts` hands this module a parsed body and writes back
|
|
6
|
+
* whatever it returns, so the dispatch is testable without a socket.
|
|
7
|
+
*/
|
|
8
|
+
/** Newest first. `initialize` echoes a client's version from this list, else answers with the first. */
|
|
9
|
+
export declare const SUPPORTED_PROTOCOL_VERSIONS: readonly ['2025-11-25', '2025-06-18', '2025-03-26'];
|
|
10
|
+
export declare const JSONRPC_PARSE_ERROR = -32700;
|
|
11
|
+
export declare const JSONRPC_INVALID_REQUEST = -32600;
|
|
12
|
+
export declare const JSONRPC_METHOD_NOT_FOUND = -32601;
|
|
13
|
+
export declare const JSONRPC_INVALID_PARAMS = -32602;
|
|
14
|
+
export declare const JSONRPC_INTERNAL_ERROR = -32603;
|
|
15
|
+
export type JsonRpcId = string | number;
|
|
16
|
+
export interface JsonRpcError {
|
|
17
|
+
code: number;
|
|
18
|
+
message: string;
|
|
19
|
+
data?: unknown;
|
|
20
|
+
}
|
|
21
|
+
export interface JsonRpcResponse {
|
|
22
|
+
jsonrpc: '2.0';
|
|
23
|
+
id: JsonRpcId | null;
|
|
24
|
+
result?: unknown;
|
|
25
|
+
error?: JsonRpcError;
|
|
26
|
+
}
|
|
27
|
+
/** A tool as `tools/list` advertises it and `tools/call` runs it. */
|
|
28
|
+
export interface ToolDefinition<TPrincipal> {
|
|
29
|
+
name: string;
|
|
30
|
+
title: string;
|
|
31
|
+
description: string;
|
|
32
|
+
inputSchema: Record<string, unknown>;
|
|
33
|
+
/** The scope a principal must hold for the tool to be listed or called. */
|
|
34
|
+
scope: string;
|
|
35
|
+
readOnly: boolean;
|
|
36
|
+
run(args: Record<string, unknown>, principal: TPrincipal): Promise<unknown>;
|
|
37
|
+
/**
|
|
38
|
+
* The text a client model reads, when JSON is the wrong shape for it — the file tools answer
|
|
39
|
+
* in the plain text their Claude Code namesakes do. The structured result is sent alongside either way.
|
|
40
|
+
*/
|
|
41
|
+
render?(data: unknown): string;
|
|
42
|
+
}
|
|
43
|
+
/** Raised by a tool for an expected refusal; becomes an `isError` result the client model can read, never a protocol error. */
|
|
44
|
+
export declare class ToolRefusal extends Error {
|
|
45
|
+
readonly code: string;
|
|
46
|
+
readonly details: Record<string, unknown> | undefined;
|
|
47
|
+
constructor(code: string, message: string, details?: Record<string, unknown>);
|
|
48
|
+
}
|
|
49
|
+
export interface DispatchContext<TPrincipal> {
|
|
50
|
+
principal: TPrincipal;
|
|
51
|
+
/** The tools this principal may see and call — already filtered by scope. */
|
|
52
|
+
tools(): readonly ToolDefinition<TPrincipal>[];
|
|
53
|
+
/**
|
|
54
|
+
* Every tool the server has, whatever the principal holds — so a call to a
|
|
55
|
+
* tool its scopes do not reach is refused as insufficient scope by
|
|
56
|
+
* `authorizeCall`, rather than reported as a tool that does not exist.
|
|
57
|
+
*/
|
|
58
|
+
lookup?(name: string): ToolDefinition<TPrincipal> | undefined;
|
|
59
|
+
instructions: string;
|
|
60
|
+
/** Re-checks membership and scope at call time (design.md D4); throws a ToolRefusal to refuse. */
|
|
61
|
+
authorizeCall(tool: ToolDefinition<TPrincipal>): void | Promise<void>;
|
|
62
|
+
/** Turns an unexpected error from a tool into a refusal the client can read, or rethrows. */
|
|
63
|
+
refusalFor(err: unknown): ToolRefusal | undefined;
|
|
64
|
+
}
|
|
65
|
+
export declare function parseErrorResponse(): JsonRpcResponse;
|
|
66
|
+
export declare function negotiateProtocolVersion(requested: unknown): string;
|
|
67
|
+
/**
|
|
68
|
+
* One POST body in, the body to answer with out — `null` when nothing in it
|
|
69
|
+
* expects an answer (only notifications and responses), which the transport
|
|
70
|
+
* turns into HTTP 202. A batch (allowed by 2025-03-26) is answered as a batch.
|
|
71
|
+
*/
|
|
72
|
+
export declare function dispatch<TPrincipal>(body: unknown, ctx: DispatchContext<TPrincipal>): Promise<JsonRpcResponse | JsonRpcResponse[] | null>;
|
|
73
|
+
//# sourceMappingURL=jsonrpc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jsonrpc.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/jsonrpc.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AAEH,wGAAwG;AACxG,eAAO,MAAM,2BAA2B,YAAI,YAAY,EAAE,YAAY,EAAE,YAAY,CAAU,CAAC;AAE/F,eAAO,MAAM,mBAAmB,SAAS,CAAC;AAC1C,eAAO,MAAM,uBAAuB,SAAS,CAAC;AAC9C,eAAO,MAAM,wBAAwB,SAAS,CAAC;AAC/C,eAAO,MAAM,sBAAsB,SAAS,CAAC;AAC7C,eAAO,MAAM,sBAAsB,SAAS,CAAC;AAE7C,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;AAExC,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,KAAK,CAAC;IACf,EAAE,EAAE,SAAS,GAAG,IAAI,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB;AAED,qEAAqE;AACrE,MAAM,WAAW,cAAc,CAAC,UAAU;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,2EAA2E;IAC3E,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;IAClB,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5E;;;OAGG;IACH,MAAM,CAAC,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC;CAChC;AAED,+HAA+H;AAC/H,qBAAa,WAAY,SAAQ,KAAK;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IAEtD,YAAY,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAK3E;CACF;AAED,MAAM,WAAW,eAAe,CAAC,UAAU;IACzC,SAAS,EAAE,UAAU,CAAC;IACtB,6EAA6E;IAC7E,KAAK,IAAI,SAAS,cAAc,CAAC,UAAU,CAAC,EAAE,CAAC;IAC/C;;;;OAIG;IACH,MAAM,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAAC,UAAU,CAAC,GAAG,SAAS,CAAC;IAC9D,YAAY,EAAE,MAAM,CAAC;IACrB,kGAAkG;IAClG,aAAa,CAAC,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtE,6FAA6F;IAC7F,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,WAAW,GAAG,SAAS,CAAC;CACnD;AAcD,wBAAgB,kBAAkB,IAAI,eAAe,CAEpD;AAED,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,OAAO,GAAG,MAAM,CAInE;AAuFD;;;;GAIG;AACH,wBAAsB,QAAQ,CAAC,UAAU,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,eAAe,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,eAAe,GAAG,eAAe,EAAE,GAAG,IAAI,CAAC,CAW/I"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { CLI_VERSION } from '../../version.js';
|
|
2
|
+
/**
|
|
3
|
+
* serve-the-store-over-mcp design.md D2: the MCP subset the remote surface
|
|
4
|
+
* speaks, hand-rolled rather than taken from the SDK. Four methods, every
|
|
5
|
+
* notification accepted and ignored, everything else refused in protocol.
|
|
6
|
+
* Transport-free: `http.ts` hands this module a parsed body and writes back
|
|
7
|
+
* whatever it returns, so the dispatch is testable without a socket.
|
|
8
|
+
*/
|
|
9
|
+
/** Newest first. `initialize` echoes a client's version from this list, else answers with the first. */
|
|
10
|
+
export const SUPPORTED_PROTOCOL_VERSIONS = ['2025-11-25', '2025-06-18', '2025-03-26'];
|
|
11
|
+
export const JSONRPC_PARSE_ERROR = -32700;
|
|
12
|
+
export const JSONRPC_INVALID_REQUEST = -32600;
|
|
13
|
+
export const JSONRPC_METHOD_NOT_FOUND = -32601;
|
|
14
|
+
export const JSONRPC_INVALID_PARAMS = -32602;
|
|
15
|
+
export const JSONRPC_INTERNAL_ERROR = -32603;
|
|
16
|
+
/** Raised by a tool for an expected refusal; becomes an `isError` result the client model can read, never a protocol error. */
|
|
17
|
+
export class ToolRefusal extends Error {
|
|
18
|
+
code;
|
|
19
|
+
details;
|
|
20
|
+
constructor(code, message, details) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = 'ToolRefusal';
|
|
23
|
+
this.code = code;
|
|
24
|
+
this.details = details;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function isObject(value) {
|
|
28
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
29
|
+
}
|
|
30
|
+
function isValidId(value) {
|
|
31
|
+
return typeof value === 'string' || (typeof value === 'number' && Number.isFinite(value));
|
|
32
|
+
}
|
|
33
|
+
function errorResponse(id, code, message, data) {
|
|
34
|
+
return { jsonrpc: '2.0', id, error: data === undefined ? { code, message } : { code, message, data } };
|
|
35
|
+
}
|
|
36
|
+
export function parseErrorResponse() {
|
|
37
|
+
return errorResponse(null, JSONRPC_PARSE_ERROR, 'Parse error: the request body is not valid JSON.');
|
|
38
|
+
}
|
|
39
|
+
export function negotiateProtocolVersion(requested) {
|
|
40
|
+
return typeof requested === 'string' && SUPPORTED_PROTOCOL_VERSIONS.includes(requested)
|
|
41
|
+
? requested
|
|
42
|
+
: SUPPORTED_PROTOCOL_VERSIONS[0];
|
|
43
|
+
}
|
|
44
|
+
function toolResult(data, isError, render) {
|
|
45
|
+
return {
|
|
46
|
+
content: [{ type: 'text', text: render !== undefined && !isError ? render(data) : JSON.stringify(data, null, 2) }],
|
|
47
|
+
structuredContent: data,
|
|
48
|
+
isError,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async function callTool(id, params, ctx) {
|
|
52
|
+
if (!isObject(params) || typeof params.name !== 'string') {
|
|
53
|
+
return errorResponse(id, JSONRPC_INVALID_PARAMS, 'tools/call needs params.name naming a tool.');
|
|
54
|
+
}
|
|
55
|
+
const name = params.name;
|
|
56
|
+
const tool = ctx.lookup?.(name) ?? ctx.tools().find((t) => t.name === name);
|
|
57
|
+
if (tool === undefined)
|
|
58
|
+
return errorResponse(id, JSONRPC_INVALID_PARAMS, `Unknown tool "${params.name}".`);
|
|
59
|
+
const args = params.arguments === undefined ? {} : params.arguments;
|
|
60
|
+
if (!isObject(args))
|
|
61
|
+
return errorResponse(id, JSONRPC_INVALID_PARAMS, 'tools/call params.arguments must be an object.');
|
|
62
|
+
try {
|
|
63
|
+
await ctx.authorizeCall(tool);
|
|
64
|
+
return { jsonrpc: '2.0', id, result: toolResult(await tool.run(args, ctx.principal), false, tool.render) };
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
const refusal = err instanceof ToolRefusal ? err : ctx.refusalFor(err);
|
|
68
|
+
if (refusal === undefined)
|
|
69
|
+
throw err;
|
|
70
|
+
const data = { error: { code: refusal.code, message: refusal.message, ...(refusal.details ? { details: refusal.details } : {}) } };
|
|
71
|
+
return { jsonrpc: '2.0', id, result: toolResult(data, true) };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function dispatchOne(message, ctx) {
|
|
75
|
+
if (!isObject(message) || message.jsonrpc !== '2.0' || typeof message.method !== 'string') {
|
|
76
|
+
// A response object sent to us (a client answering a request we never made) is not a request; drop it.
|
|
77
|
+
if (isObject(message) && message.jsonrpc === '2.0' && message.method === undefined && ('result' in message || 'error' in message)) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const id = isObject(message) && isValidId(message.id) ? message.id : null;
|
|
81
|
+
return errorResponse(id, JSONRPC_INVALID_REQUEST, 'Invalid request: expected a JSON-RPC 2.0 request object.');
|
|
82
|
+
}
|
|
83
|
+
// A notification has no id and gets no answer, whatever its method.
|
|
84
|
+
if (!('id' in message))
|
|
85
|
+
return null;
|
|
86
|
+
if (!isValidId(message.id))
|
|
87
|
+
return errorResponse(null, JSONRPC_INVALID_REQUEST, 'Invalid request: id must be a string or a number.');
|
|
88
|
+
const id = message.id;
|
|
89
|
+
try {
|
|
90
|
+
switch (message.method) {
|
|
91
|
+
case 'initialize': {
|
|
92
|
+
const params = isObject(message.params) ? message.params : {};
|
|
93
|
+
return {
|
|
94
|
+
jsonrpc: '2.0',
|
|
95
|
+
id,
|
|
96
|
+
result: {
|
|
97
|
+
protocolVersion: negotiateProtocolVersion(params.protocolVersion),
|
|
98
|
+
capabilities: { tools: { listChanged: false } },
|
|
99
|
+
serverInfo: { name: 'contexture', title: 'contexture store', version: CLI_VERSION },
|
|
100
|
+
instructions: ctx.instructions,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
case 'ping':
|
|
105
|
+
return { jsonrpc: '2.0', id, result: {} };
|
|
106
|
+
case 'tools/list':
|
|
107
|
+
return {
|
|
108
|
+
jsonrpc: '2.0',
|
|
109
|
+
id,
|
|
110
|
+
result: {
|
|
111
|
+
tools: ctx.tools().map((tool) => ({
|
|
112
|
+
name: tool.name,
|
|
113
|
+
title: tool.title,
|
|
114
|
+
description: tool.description,
|
|
115
|
+
inputSchema: tool.inputSchema,
|
|
116
|
+
annotations: { title: tool.title, readOnlyHint: tool.readOnly, destructiveHint: false, idempotentHint: tool.readOnly, openWorldHint: false },
|
|
117
|
+
})),
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
case 'tools/call':
|
|
121
|
+
return await callTool(id, message.params, ctx);
|
|
122
|
+
default:
|
|
123
|
+
return errorResponse(id, JSONRPC_METHOD_NOT_FOUND, `Method not found: "${message.method}".`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
return errorResponse(id, JSONRPC_INTERNAL_ERROR, err instanceof Error ? err.message : String(err));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* One POST body in, the body to answer with out — `null` when nothing in it
|
|
132
|
+
* expects an answer (only notifications and responses), which the transport
|
|
133
|
+
* turns into HTTP 202. A batch (allowed by 2025-03-26) is answered as a batch.
|
|
134
|
+
*/
|
|
135
|
+
export async function dispatch(body, ctx) {
|
|
136
|
+
if (Array.isArray(body)) {
|
|
137
|
+
if (body.length === 0)
|
|
138
|
+
return errorResponse(null, JSONRPC_INVALID_REQUEST, 'Invalid request: an empty batch.');
|
|
139
|
+
const answers = [];
|
|
140
|
+
for (const message of body) {
|
|
141
|
+
const answer = await dispatchOne(message, ctx);
|
|
142
|
+
if (answer !== null)
|
|
143
|
+
answers.push(answer);
|
|
144
|
+
}
|
|
145
|
+
return answers.length === 0 ? null : answers;
|
|
146
|
+
}
|
|
147
|
+
return dispatchOne(body, ctx);
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=jsonrpc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jsonrpc.js","sourceRoot":"","sources":["../../../src/core/mcp/jsonrpc.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { McpConfig } from '../../config/schema.js';
|
|
2
|
+
/**
|
|
3
|
+
* remote-access spec, *Scopes follow the configuration on every call*: a
|
|
4
|
+
* verified email holds `read` when `mcp.readers` admits it, `capture` when
|
|
5
|
+
* `mcp.capturers` does and `edit` when `mcp.editors` does, matched by the same rule `change_approval.deciders`
|
|
6
|
+
* is matched by — so an `@domain` entry admits that domain only.
|
|
7
|
+
*/
|
|
8
|
+
export declare const MCP_SCOPES: readonly ['read', 'capture', 'edit'];
|
|
9
|
+
export type McpScope = (typeof MCP_SCOPES)[number];
|
|
10
|
+
export declare function scopesFor(email: string, mcp: McpConfig | undefined): McpScope[];
|
|
11
|
+
//# sourceMappingURL=members.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"members.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/members.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAGxD;;;;;GAKG;AACH,eAAO,MAAM,UAAU,YAAI,MAAM,EAAE,SAAS,EAAE,MAAM,CAAU,CAAC;AAC/D,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAEnD,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,GAAG,SAAS,GAAG,QAAQ,EAAE,CAO/E"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { isDecider } from '../approval/decision.js';
|
|
2
|
+
/**
|
|
3
|
+
* remote-access spec, *Scopes follow the configuration on every call*: a
|
|
4
|
+
* verified email holds `read` when `mcp.readers` admits it, `capture` when
|
|
5
|
+
* `mcp.capturers` does and `edit` when `mcp.editors` does, matched by the same rule `change_approval.deciders`
|
|
6
|
+
* is matched by — so an `@domain` entry admits that domain only.
|
|
7
|
+
*/
|
|
8
|
+
export const MCP_SCOPES = ['read', 'capture', 'edit'];
|
|
9
|
+
export function scopesFor(email, mcp) {
|
|
10
|
+
if (mcp === undefined)
|
|
11
|
+
return [];
|
|
12
|
+
const scopes = [];
|
|
13
|
+
if (isDecider(email, mcp.readers ?? []))
|
|
14
|
+
scopes.push('read');
|
|
15
|
+
if (isDecider(email, mcp.capturers ?? []))
|
|
16
|
+
scopes.push('capture');
|
|
17
|
+
if (isDecider(email, mcp.editors ?? []))
|
|
18
|
+
scopes.push('edit');
|
|
19
|
+
return scopes;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=members.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"members.js","sourceRoot":"","sources":["../../../src/core/mcp/members.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEpD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,CAAU,CAAC;AAG/D,MAAM,UAAU,SAAS,CAAC,KAAa,EAAE,GAA0B;IACjE,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IACjC,MAAM,MAAM,GAAe,EAAE,CAAC;IAC9B,IAAI,SAAS,CAAC,KAAK,EAAE,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC;QAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC7D,IAAI,SAAS,CAAC,KAAK,EAAE,GAAG,CAAC,SAAS,IAAI,EAAE,CAAC;QAAE,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAClE,IAAI,SAAS,CAAC,KAAK,EAAE,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC;QAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC7D,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import type { McpConfig } from '../../config/schema.js';
|
|
3
|
+
import type { IdentityVerifier } from '../approval/identity.js';
|
|
4
|
+
import { type Authentication } from './http.js';
|
|
5
|
+
import { type McpScope } from './members.js';
|
|
6
|
+
/**
|
|
7
|
+
* serve-the-store-over-mcp design.md D4–D5: `ctxr serve` is the OAuth 2.1
|
|
8
|
+
* authorisation server for its own `/mcp`, and it stores nothing it issues.
|
|
9
|
+
* Client ids, authorisation codes and access tokens are HMAC-signed payloads.
|
|
10
|
+
* Who may sign in is decided by the access proxy's verified identity — the
|
|
11
|
+
* assertion `change_approval.identity` configures, checked on every
|
|
12
|
+
* `/authorize` request exactly as the approval routes check it — and by the
|
|
13
|
+
* `mcp` block the server last loaded, consulted again on every `/mcp`
|
|
14
|
+
* request, so removing someone there refuses their next call.
|
|
15
|
+
*/
|
|
16
|
+
export declare const ACCESS_TOKEN_TTL_SECONDS: number;
|
|
17
|
+
export declare const AUTHORIZATION_CODE_TTL_SECONDS: number;
|
|
18
|
+
export declare const PROTECTED_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource";
|
|
19
|
+
export declare const AUTHORIZATION_SERVER_METADATA_PATH = "/.well-known/oauth-authorization-server";
|
|
20
|
+
export declare const OPENID_CONFIGURATION_PATH = "/.well-known/openid-configuration";
|
|
21
|
+
export declare const REGISTER_PATH = "/register";
|
|
22
|
+
export declare const AUTHORIZE_PATH = "/authorize";
|
|
23
|
+
export declare const TOKEN_PATH = "/token";
|
|
24
|
+
/** Every path the OAuth side of the MCP surface answers. */
|
|
25
|
+
export declare const OAUTH_PATHS: readonly string[];
|
|
26
|
+
/** Who an admitted `/mcp` request acts as. `scopes` is already narrowed to what the email holds right now. */
|
|
27
|
+
export interface Principal {
|
|
28
|
+
email: string;
|
|
29
|
+
scopes: McpScope[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* `kind.payload.mac`, the kind inside the MAC so a client id can never be
|
|
33
|
+
* replayed as a token. Authorisation codes are signed under a key that mixes
|
|
34
|
+
* in a per-process nonce: a restart invalidates outstanding codes instead of
|
|
35
|
+
* forgetting which were spent (design.md D5).
|
|
36
|
+
*/
|
|
37
|
+
export declare class Signer {
|
|
38
|
+
private readonly key;
|
|
39
|
+
private readonly codeKey;
|
|
40
|
+
constructor(key: Buffer);
|
|
41
|
+
private keyFor;
|
|
42
|
+
sign(kind: string, payload: object): string;
|
|
43
|
+
verify<T>(kind: string, token: string): T | undefined;
|
|
44
|
+
}
|
|
45
|
+
export interface ConsentPageView {
|
|
46
|
+
clientName: string;
|
|
47
|
+
redirectHost: string;
|
|
48
|
+
email: string;
|
|
49
|
+
scopes: McpScope[];
|
|
50
|
+
hiddenFields: Record<string, string>;
|
|
51
|
+
}
|
|
52
|
+
export interface OAuthOptions {
|
|
53
|
+
/** The origin of `serve.base_url`: the issuer, and the only `Origin` a consent `POST` may carry. */
|
|
54
|
+
issuer: string;
|
|
55
|
+
/** `<issuer>/mcp`: the one resource tokens are bound to. */
|
|
56
|
+
resource: string;
|
|
57
|
+
/** `change_approval.identity.header`. */
|
|
58
|
+
identityHeader: string;
|
|
59
|
+
verifier: IdentityVerifier;
|
|
60
|
+
/** The `mcp` block of the configuration the server last loaded. */
|
|
61
|
+
mcp(): McpConfig | undefined;
|
|
62
|
+
signer: Signer;
|
|
63
|
+
/** Nonces of redeemed codes, kept by the server across configuration reloads. */
|
|
64
|
+
spentCodes: Map<string, number>;
|
|
65
|
+
now(): Date;
|
|
66
|
+
renderConsentPage(view: ConsentPageView): Promise<string>;
|
|
67
|
+
/** One line per registration attempt: what the client asked for and the outcome, never a secret. */
|
|
68
|
+
log?(line: string): void;
|
|
69
|
+
}
|
|
70
|
+
/** The OAuth endpoints of the MCP surface. Built per request from the loaded configuration; holds no state of its own. */
|
|
71
|
+
export declare class AuthorizationServer {
|
|
72
|
+
private readonly options;
|
|
73
|
+
constructor(options: OAuthOptions);
|
|
74
|
+
private nowSeconds;
|
|
75
|
+
get protectedResourceMetadataUrl(): string;
|
|
76
|
+
protectedResourceMetadata(): Record<string, unknown>;
|
|
77
|
+
authorizationServerMetadata(): Record<string, unknown>;
|
|
78
|
+
/** Handles every OAuth path; returns false for anything else. */
|
|
79
|
+
handle(req: IncomingMessage, res: ServerResponse, pathname: string): Promise<boolean>;
|
|
80
|
+
private register;
|
|
81
|
+
/**
|
|
82
|
+
* Until the client and redirect URI are known good, an error is shown and
|
|
83
|
+
* never redirected — redirecting to an unverified URI is how an open
|
|
84
|
+
* redirector is built.
|
|
85
|
+
*/
|
|
86
|
+
private validateAuthorizeRequest;
|
|
87
|
+
/** The verified member and the scopes this request would grant, or why there is none. */
|
|
88
|
+
private member;
|
|
89
|
+
private hiddenFieldsFor;
|
|
90
|
+
private authorizeForm;
|
|
91
|
+
/** The member's confirmation: only from a page this server rendered (`Origin`), only for a verified member. */
|
|
92
|
+
private authorizeConfirm;
|
|
93
|
+
private token;
|
|
94
|
+
private pruneSpent;
|
|
95
|
+
/** remote-access spec: every `/mcp` request carries a token this server issued, narrowed to what its email holds now. */
|
|
96
|
+
authenticate(req: IncomingMessage): Promise<Authentication<Principal>>;
|
|
97
|
+
principalFor(token: string): Principal | undefined;
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=oauth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/oauth.ts"],"names":[],"mappings":"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"}
|