@ultimat3/mcp 1.0.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.
@@ -0,0 +1,76 @@
1
+ // stdio transport for `x mcp serve` — newline-delimited JSON-RPC on stdin/stdout.
2
+ //
3
+ // A local agent (Claude Code, an editor) launches the process and speaks over the pipe, so
4
+ // there is no token to resolve: the caller already has the developer's shell. The caller is
5
+ // therefore constructed once from the local developer identity the CLI supplies, and its
6
+ // scopes are whatever that developer is entitled to — no network boundary to defend.
7
+ //
8
+ // stdout is the WIRE. Anything logged there corrupts the protocol, so diagnostics go to
9
+ // stderr and this file never calls `console.log`.
10
+
11
+ import type { McpCaller } from './registry';
12
+ import type { McpServer } from './server';
13
+ import { errorResponse, PARSE_ERROR } from './wire';
14
+
15
+ export interface StdioTransportInput {
16
+ readonly server: McpServer;
17
+ /** The local developer, already resolved by the CLI. Usually `kind: 'agent'`. */
18
+ readonly caller: McpCaller;
19
+ /** Defaults to `Bun.stdin.stream()`; overridable so a test can feed a fixed script. */
20
+ readonly input?: ReadableStream<Uint8Array>;
21
+ /** Defaults to writing `Bun.stdout`. */
22
+ write?(chunk: string): Promise<void> | void;
23
+ }
24
+
25
+ /**
26
+ * Serve until stdin closes. Resolves when the peer disconnects, which is the CLI's signal
27
+ * to exit 0 — a closed pipe is a normal shutdown, not an error.
28
+ */
29
+ export async function serveStdio(config: StdioTransportInput): Promise<void> {
30
+ const stream = config.input ?? Bun.stdin.stream();
31
+ const write = config.write ?? defaultWrite;
32
+ const decoder = new TextDecoder();
33
+ let buffer = '';
34
+
35
+ for await (const chunk of stream) {
36
+ buffer += decoder.decode(chunk, { stream: true });
37
+ let newline = buffer.indexOf('\n');
38
+ while (newline !== -1) {
39
+ const line = buffer.slice(0, newline);
40
+ buffer = buffer.slice(newline + 1);
41
+ await handleLine(config.server, config.caller, line, write);
42
+ newline = buffer.indexOf('\n');
43
+ }
44
+ }
45
+ // A trailing message with no newline is still a message.
46
+ if (buffer.trim().length > 0) {
47
+ await handleLine(config.server, config.caller, buffer, write);
48
+ }
49
+ }
50
+
51
+ async function handleLine(
52
+ server: McpServer,
53
+ caller: McpCaller,
54
+ line: string,
55
+ write: (chunk: string) => Promise<void> | void,
56
+ ): Promise<void> {
57
+ const trimmed = line.trim();
58
+ if (trimmed.length === 0) return;
59
+
60
+ let body: unknown;
61
+ try {
62
+ body = JSON.parse(trimmed);
63
+ } catch {
64
+ await write(`${JSON.stringify(errorResponse(null, PARSE_ERROR, 'invalid JSON line'))}\n`);
65
+ return;
66
+ }
67
+
68
+ const response = await server.handle(body, caller);
69
+ // `null` means notification: emit nothing at all, or the peer sees a phantom reply.
70
+ if (response === null) return;
71
+ await write(`${JSON.stringify(response)}\n`);
72
+ }
73
+
74
+ function defaultWrite(chunk: string): void {
75
+ Bun.stdout.write(chunk);
76
+ }
@@ -0,0 +1,161 @@
1
+ // Argument validation against a tool's declared JSON Schema — the ONE arg contract.
2
+ //
3
+ // Why not a second validator alongside the schema: `tools/list` hands the agent a JSON
4
+ // Schema, so that document must be the thing enforced. If a tool could also carry a
5
+ // private validator the agent would be judged against a contract it never saw. Actions
6
+ // still re-parse authoritatively inside their own handler; this pass exists so a wrong
7
+ // call comes back as a structured issue list instead of a round trip.
8
+
9
+ import type { JsonSchema } from './wire';
10
+
11
+ export interface ArgIssue {
12
+ /** Dotted path from the arguments root, `''` for the root itself. */
13
+ readonly path: string;
14
+ readonly message: string;
15
+ }
16
+
17
+ export type ArgValidation =
18
+ | { readonly ok: true; readonly value: Record<string, unknown> }
19
+ | { readonly ok: false; readonly issues: readonly ArgIssue[] };
20
+
21
+ /**
22
+ * Validate `raw` against `schema`, applying declared `default`s. Returns the coerced
23
+ * record so a handler reads defaults without repeating them.
24
+ */
25
+ export function validateArgs(schema: JsonSchema, raw: unknown): ArgValidation {
26
+ const issues: ArgIssue[] = [];
27
+ const value = walk(schema, raw ?? {}, '', issues);
28
+ if (issues.length > 0) return { ok: false, issues };
29
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
30
+ return { ok: false, issues: [{ path: '', message: 'arguments must be an object' }] };
31
+ }
32
+ return { ok: true, value: value as Record<string, unknown> };
33
+ }
34
+
35
+ function walk(schema: JsonSchema, input: unknown, path: string, issues: ArgIssue[]): unknown {
36
+ if (schema.anyOf !== undefined) return anyOf(schema.anyOf, input, path, issues);
37
+ if (schema.const !== undefined && input !== schema.const) {
38
+ issues.push({ path, message: `must equal ${JSON.stringify(schema.const)}` });
39
+ return input;
40
+ }
41
+ if (schema.enum !== undefined && !schema.enum.includes(input as string)) {
42
+ issues.push({ path, message: `must be one of ${schema.enum.map(String).join(' | ')}` });
43
+ return input;
44
+ }
45
+ switch (schema.type) {
46
+ case 'object':
47
+ return object(schema, input, path, issues);
48
+ case 'array':
49
+ return array(schema, input, path, issues);
50
+ case 'string':
51
+ return string(schema, input, path, issues);
52
+ case 'number':
53
+ case 'integer':
54
+ return number(schema, input, path, issues);
55
+ case 'boolean':
56
+ if (typeof input !== 'boolean') issues.push({ path, message: 'must be a boolean' });
57
+ return input;
58
+ case 'null':
59
+ if (input !== null) issues.push({ path, message: 'must be null' });
60
+ return input;
61
+ default:
62
+ return input;
63
+ }
64
+ }
65
+
66
+ function object(
67
+ schema: JsonSchema,
68
+ input: unknown,
69
+ path: string,
70
+ issues: ArgIssue[],
71
+ ): Record<string, unknown> | unknown {
72
+ if (typeof input !== 'object' || input === null || Array.isArray(input)) {
73
+ issues.push({ path, message: 'must be an object' });
74
+ return input;
75
+ }
76
+ const source = input as Record<string, unknown>;
77
+ const properties = schema.properties ?? {};
78
+ const out: Record<string, unknown> = {};
79
+
80
+ for (const key of Object.keys(source)) {
81
+ if (properties[key] === undefined) {
82
+ if (schema.additionalProperties === false) {
83
+ issues.push({ path: join(path, key), message: 'unknown property' });
84
+ continue;
85
+ }
86
+ out[key] = source[key];
87
+ }
88
+ }
89
+ for (const [key, child] of Object.entries(properties)) {
90
+ const at = join(path, key);
91
+ const present = Object.hasOwn(source, key) && source[key] !== undefined;
92
+ if (!present) {
93
+ if (child.default !== undefined) out[key] = child.default;
94
+ else if (schema.required?.includes(key) === true) {
95
+ issues.push({ path: at, message: 'is required' });
96
+ }
97
+ continue;
98
+ }
99
+ out[key] = walk(child, source[key], at, issues);
100
+ }
101
+ return out;
102
+ }
103
+
104
+ function array(schema: JsonSchema, input: unknown, path: string, issues: ArgIssue[]): unknown {
105
+ if (!Array.isArray(input)) {
106
+ issues.push({ path, message: 'must be an array' });
107
+ return input;
108
+ }
109
+ const items = schema.items;
110
+ if (items === undefined) return input;
111
+ return input.map((item, index) => walk(items, item, `${path}[${index}]`, issues));
112
+ }
113
+
114
+ function string(schema: JsonSchema, input: unknown, path: string, issues: ArgIssue[]): unknown {
115
+ if (typeof input !== 'string') {
116
+ issues.push({ path, message: 'must be a string' });
117
+ return input;
118
+ }
119
+ if (schema.minLength !== undefined && input.length < schema.minLength) {
120
+ issues.push({ path, message: `must be at least ${schema.minLength} characters` });
121
+ }
122
+ if (schema.maxLength !== undefined && input.length > schema.maxLength) {
123
+ issues.push({ path, message: `must be at most ${schema.maxLength} characters` });
124
+ }
125
+ return input;
126
+ }
127
+
128
+ function number(schema: JsonSchema, input: unknown, path: string, issues: ArgIssue[]): unknown {
129
+ if (typeof input !== 'number' || Number.isNaN(input)) {
130
+ issues.push({ path, message: 'must be a number' });
131
+ return input;
132
+ }
133
+ if (schema.type === 'integer' && !Number.isInteger(input)) {
134
+ issues.push({ path, message: 'must be an integer' });
135
+ }
136
+ if (schema.minimum !== undefined && input < schema.minimum) {
137
+ issues.push({ path, message: `must be >= ${schema.minimum}` });
138
+ }
139
+ if (schema.maximum !== undefined && input > schema.maximum) {
140
+ issues.push({ path, message: `must be <= ${schema.maximum}` });
141
+ }
142
+ return input;
143
+ }
144
+
145
+ /** First branch that validates wins; if none does, report the union, not each branch. */
146
+ function anyOf(
147
+ branches: readonly JsonSchema[],
148
+ input: unknown,
149
+ path: string,
150
+ issues: ArgIssue[],
151
+ ): unknown {
152
+ for (const branch of branches) {
153
+ const local: ArgIssue[] = [];
154
+ const value = walk(branch, input, path, local);
155
+ if (local.length === 0) return value;
156
+ }
157
+ issues.push({ path, message: `does not match any of the ${branches.length} allowed shapes` });
158
+ return input;
159
+ }
160
+
161
+ const join = (path: string, key: string): string => (path === '' ? key : `${path}.${key}`);
package/src/wire.ts ADDED
@@ -0,0 +1,115 @@
1
+ // JSON-RPC 2.0 vocabulary for the Ultimate MCP server: envelope types, the standard
2
+ // error codes, the advertised protocol version, and the two response constructors.
3
+ // Pure data + pure functions so every transport (http, stdio, a test) agrees on the wire
4
+ // without importing the server.
5
+ //
6
+ // Reference: https://modelcontextprotocol.io/specification (2025-06-18).
7
+
8
+ import { FRAMEWORK_VERSION } from '@ultimat3/core';
9
+
10
+ /** MCP protocol version advertised on `initialize`. */
11
+ export const MCP_PROTOCOL_VERSION = '2025-06-18';
12
+
13
+ /** Identity advertised on `initialize` unless the host overrides it. */
14
+ export const DEFAULT_SERVER_INFO = { name: 'ultimate', version: FRAMEWORK_VERSION } as const;
15
+
16
+ export interface ServerInfo {
17
+ readonly name: string;
18
+ readonly version: string;
19
+ }
20
+
21
+ export type JsonRpcId = string | number | null;
22
+
23
+ /** A request OR a notification — a notification is exactly "no `id`". */
24
+ export interface JsonRpcRequest {
25
+ readonly jsonrpc: '2.0';
26
+ readonly method: string;
27
+ readonly params?: unknown;
28
+ readonly id?: JsonRpcId;
29
+ }
30
+
31
+ export interface JsonRpcError {
32
+ readonly code: number;
33
+ readonly message: string;
34
+ readonly data?: unknown;
35
+ }
36
+
37
+ export interface JsonRpcResponse {
38
+ readonly jsonrpc: '2.0';
39
+ readonly id: JsonRpcId;
40
+ readonly result?: unknown;
41
+ readonly error?: JsonRpcError;
42
+ }
43
+
44
+ // ── standard error codes (jsonrpc.org/spec) ──────────────────────────────────
45
+ //
46
+ // The MCP spec adds no codes of its own, so the two security answers map onto these:
47
+ // INVALID_REQUEST (-32600) → Forbidden: the tool exists and you may see it, but your
48
+ // token lacks its scope.
49
+ // METHOD_NOT_FOUND (-32601) → ToolNotFound: the tool is absent OR hidden from your role.
50
+ // Never the other way round — see `registry.ts`.
51
+
52
+ export const PARSE_ERROR = -32700;
53
+ export const INVALID_REQUEST = -32600;
54
+ export const METHOD_NOT_FOUND = -32601;
55
+ export const INVALID_PARAMS = -32602;
56
+ export const INTERNAL_ERROR = -32603;
57
+
58
+ /**
59
+ * The JSON Schema subset the framework emits and `validate-args.ts` enforces. Narrow on
60
+ * purpose: a tool schema an agent cannot fully understand is a tool it will call wrong.
61
+ */
62
+ export interface JsonSchema {
63
+ readonly type?: 'object' | 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'null';
64
+ readonly title?: string;
65
+ readonly description?: string;
66
+ readonly properties?: Readonly<Record<string, JsonSchema>>;
67
+ readonly required?: readonly string[];
68
+ readonly additionalProperties?: boolean;
69
+ readonly items?: JsonSchema;
70
+ readonly enum?: readonly (string | number | boolean | null)[];
71
+ readonly const?: string | number | boolean | null;
72
+ readonly default?: unknown;
73
+ readonly format?: string;
74
+ readonly minimum?: number;
75
+ readonly maximum?: number;
76
+ readonly minLength?: number;
77
+ readonly maxLength?: number;
78
+ readonly anyOf?: readonly JsonSchema[];
79
+ }
80
+
81
+ /** An empty-object schema — the honest shape for a no-argument tool. */
82
+ export const NO_ARGS: JsonSchema = { type: 'object', properties: {}, additionalProperties: false };
83
+
84
+ export function resultResponse(id: JsonRpcId, result: unknown): JsonRpcResponse {
85
+ return { jsonrpc: '2.0', id, result };
86
+ }
87
+
88
+ export function errorResponse(
89
+ id: JsonRpcId,
90
+ code: number,
91
+ message: string,
92
+ data?: unknown,
93
+ ): JsonRpcResponse {
94
+ // exactOptionalPropertyTypes: attach `data` only when supplied.
95
+ const error: JsonRpcError = { code, message, ...(data !== undefined ? { data } : {}) };
96
+ return { jsonrpc: '2.0', id, error };
97
+ }
98
+
99
+ /** Minimal envelope check. A body failing this is `-32600`, never a crash. */
100
+ export function isJsonRpcRequest(value: unknown): value is JsonRpcRequest {
101
+ if (typeof value !== 'object' || value === null) return false;
102
+ const o = value as Record<string, unknown>;
103
+ return o['jsonrpc'] === '2.0' && typeof o['method'] === 'string';
104
+ }
105
+
106
+ /** A notification carries no `id`; the server answers nothing and the transport 202s. */
107
+ export function isNotification(req: JsonRpcRequest): boolean {
108
+ return req.id === undefined;
109
+ }
110
+
111
+ /** `params` as a record, or `null` when the caller sent a non-object. */
112
+ export function paramsOf(req: JsonRpcRequest): Record<string, unknown> | null {
113
+ if (typeof req.params !== 'object' || req.params === null) return null;
114
+ return req.params as Record<string, unknown>;
115
+ }