@ziprack/sdk 0.1.2 → 0.2.1

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,33 @@
1
+ import type { Readable, Writable } from 'node:stream';
2
+ /**
3
+ * MCP over stdio is line-delimited JSON-RPC 2.0. That is little enough protocol
4
+ * to own outright, which keeps the studio dependency-free the way the SDK is.
5
+ */
6
+ export interface RpcRequest {
7
+ jsonrpc: '2.0';
8
+ id?: string | number | null;
9
+ method: string;
10
+ params?: Record<string, unknown>;
11
+ }
12
+ export interface RpcError {
13
+ code: number;
14
+ message: string;
15
+ data?: unknown;
16
+ }
17
+ export declare const PARSE_ERROR = -32700;
18
+ export declare const INVALID_REQUEST = -32600;
19
+ export declare const METHOD_NOT_FOUND = -32601;
20
+ export declare const INVALID_PARAMS = -32602;
21
+ export declare const INTERNAL_ERROR = -32603;
22
+ export declare class RpcFailure extends Error {
23
+ readonly code: number;
24
+ readonly data?: unknown | undefined;
25
+ constructor(code: number, message: string, data?: unknown | undefined);
26
+ }
27
+ export type RpcHandler = (params: Record<string, unknown>, method: string) => Promise<unknown> | unknown;
28
+ export interface RpcConnection {
29
+ /** Resolves when the input stream ends. */
30
+ done: Promise<void>;
31
+ notify(method: string, params?: Record<string, unknown>): void;
32
+ }
33
+ export declare function serve(input: Readable, output: Writable, handlers: Map<string, RpcHandler>): RpcConnection;
@@ -0,0 +1,86 @@
1
+ export const PARSE_ERROR = -32700;
2
+ export const INVALID_REQUEST = -32600;
3
+ export const METHOD_NOT_FOUND = -32601;
4
+ export const INVALID_PARAMS = -32602;
5
+ export const INTERNAL_ERROR = -32603;
6
+ export class RpcFailure extends Error {
7
+ code;
8
+ data;
9
+ constructor(code, message, data) {
10
+ super(message);
11
+ this.code = code;
12
+ this.data = data;
13
+ }
14
+ }
15
+ export function serve(input, output, handlers) {
16
+ const write = (payload) => {
17
+ output.write(`${JSON.stringify(payload)}\n`);
18
+ };
19
+ // One request at a time. Tools read and write the document, so overlapping
20
+ // them would let a later call answer from the state before an earlier one's
21
+ // save — the kind of ordering bug an assistant cannot see or work around.
22
+ let queue = Promise.resolve();
23
+ const done = new Promise((resolve) => {
24
+ let buffer = '';
25
+ input.setEncoding('utf8');
26
+ input.on('data', (chunk) => {
27
+ buffer += chunk;
28
+ let newline = buffer.indexOf('\n');
29
+ while (newline !== -1) {
30
+ const line = buffer.slice(0, newline).trim();
31
+ buffer = buffer.slice(newline + 1);
32
+ if (line !== '') {
33
+ queue = queue.then(() => handle(line, handlers, write));
34
+ }
35
+ newline = buffer.indexOf('\n');
36
+ }
37
+ });
38
+ // The queue is drained before the caller is told the connection is over,
39
+ // so a request on the last line still gets its answer.
40
+ input.on('end', () => void queue.then(resolve));
41
+ input.on('close', () => void queue.then(resolve));
42
+ });
43
+ return {
44
+ done,
45
+ notify: (method, params) => write({ jsonrpc: '2.0', method, params: params ?? {} }),
46
+ };
47
+ }
48
+ async function handle(line, handlers, write) {
49
+ let request;
50
+ try {
51
+ request = JSON.parse(line);
52
+ }
53
+ catch {
54
+ write({ jsonrpc: '2.0', id: null, error: { code: PARSE_ERROR, message: 'Invalid JSON.' } });
55
+ return;
56
+ }
57
+ if (typeof request.method !== 'string') {
58
+ write({ jsonrpc: '2.0', id: request.id ?? null, error: { code: INVALID_REQUEST, message: 'Missing method.' } });
59
+ return;
60
+ }
61
+ const handler = handlers.get(request.method);
62
+ // A notification has no id and, per JSON-RPC, gets no answer at all —
63
+ // including for `notifications/initialized`, which every client sends.
64
+ const isNotification = request.id === undefined || request.id === null;
65
+ if (handler === undefined) {
66
+ if (!isNotification) {
67
+ write({ jsonrpc: '2.0', id: request.id, error: { code: METHOD_NOT_FOUND, message: `Unknown method [${request.method}].` } });
68
+ }
69
+ return;
70
+ }
71
+ try {
72
+ const result = await handler(request.params ?? {}, request.method);
73
+ if (!isNotification) {
74
+ write({ jsonrpc: '2.0', id: request.id, result });
75
+ }
76
+ }
77
+ catch (error) {
78
+ if (isNotification) {
79
+ return;
80
+ }
81
+ const failure = error instanceof RpcFailure
82
+ ? { code: error.code, message: error.message, data: error.data }
83
+ : { code: INTERNAL_ERROR, message: error instanceof Error ? error.message : String(error) };
84
+ write({ jsonrpc: '2.0', id: request.id, error: failure });
85
+ }
86
+ }
@@ -0,0 +1,23 @@
1
+ import { type RpcHandler } from './rpc.js';
2
+ import { type ToolDefinition } from './tools.js';
3
+ export declare const PROTOCOL_VERSION = "2024-11-05";
4
+ export interface ServerInfo {
5
+ name: string;
6
+ version: string;
7
+ instructions: string;
8
+ }
9
+ export declare const DOCS_INSTRUCTIONS = "zipRACK applet docs, matched to the installed @ziprack/sdk. Call ziprack_guidelines before writing or changing applet code, and ziprack_search_docs before guessing at an SDK method, manifest field or error code.";
10
+ /** Wires a tool set onto the MCP method surface a client expects. */
11
+ export declare function session(tools: ToolDefinition[], info: ServerInfo): Map<string, RpcHandler>;
12
+ /**
13
+ * Tool results are text content blocks. Structured payloads go through as
14
+ * indented JSON, which is what an assistant can actually read back.
15
+ */
16
+ export declare function content(payload: unknown): {
17
+ content: Array<{
18
+ type: 'text';
19
+ text: string;
20
+ }>;
21
+ isError?: boolean;
22
+ };
23
+ export declare function startDocsMcp(): Promise<void>;
@@ -0,0 +1,55 @@
1
+ import { INVALID_PARAMS, RpcFailure, serve } from './rpc.js';
2
+ import { SDK_VERSION, docsTools } from './tools.js';
3
+ export const PROTOCOL_VERSION = '2024-11-05';
4
+ export const DOCS_INSTRUCTIONS = 'zipRACK applet docs, matched to the installed @ziprack/sdk. Call ziprack_guidelines before writing or changing applet code, and ziprack_search_docs before guessing at an SDK method, manifest field or error code.';
5
+ /** Wires a tool set onto the MCP method surface a client expects. */
6
+ export function session(tools, info) {
7
+ const byName = new Map(tools.map((tool) => [tool.name, tool]));
8
+ const handlers = new Map();
9
+ handlers.set('initialize', () => ({
10
+ protocolVersion: PROTOCOL_VERSION,
11
+ capabilities: { tools: { listChanged: false } },
12
+ serverInfo: { name: info.name, version: info.version },
13
+ instructions: info.instructions,
14
+ }));
15
+ handlers.set('notifications/initialized', () => null);
16
+ handlers.set('ping', () => ({}));
17
+ handlers.set('tools/list', () => ({
18
+ tools: tools.map((tool) => ({
19
+ name: tool.name,
20
+ description: tool.description,
21
+ inputSchema: tool.inputSchema,
22
+ })),
23
+ }));
24
+ handlers.set('tools/call', async (params) => {
25
+ const name = params.name;
26
+ if (typeof name !== 'string') {
27
+ throw new RpcFailure(INVALID_PARAMS, 'tools/call needs a tool name.');
28
+ }
29
+ const tool = byName.get(name);
30
+ if (tool === undefined) {
31
+ throw new RpcFailure(INVALID_PARAMS, `Unknown tool [${name}].`);
32
+ }
33
+ const args = params.arguments;
34
+ return content(await tool.run(args === null || typeof args !== 'object' ? {} : args));
35
+ });
36
+ return handlers;
37
+ }
38
+ /**
39
+ * Tool results are text content blocks. Structured payloads go through as
40
+ * indented JSON, which is what an assistant can actually read back.
41
+ */
42
+ export function content(payload) {
43
+ if (typeof payload === 'string') {
44
+ return { content: [{ type: 'text', text: payload }] };
45
+ }
46
+ if (payload !== null && typeof payload === 'object' && 'isError' in payload) {
47
+ const { isError, ...rest } = payload;
48
+ return { content: [{ type: 'text', text: JSON.stringify(rest, null, 2) }], isError };
49
+ }
50
+ return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
51
+ }
52
+ export async function startDocsMcp() {
53
+ const handlers = session(docsTools(), { name: 'ziprack', version: SDK_VERSION, instructions: DOCS_INSTRUCTIONS });
54
+ await serve(process.stdin, process.stdout, handlers).done;
55
+ }
@@ -0,0 +1,20 @@
1
+ import { type Doc } from './docs.js';
2
+ export interface ToolDefinition {
3
+ name: string;
4
+ description: string;
5
+ inputSchema: {
6
+ type: 'object';
7
+ properties?: Record<string, object>;
8
+ required?: string[];
9
+ additionalProperties?: boolean;
10
+ };
11
+ /** Returned as the tool result; objects are serialised as JSON text. */
12
+ run(args: Record<string, unknown>): Promise<unknown> | unknown;
13
+ }
14
+ export declare const SDK_VERSION: string;
15
+ /**
16
+ * Docs and guidelines for the SDK version this server was installed with.
17
+ * Every answer names that version, so an assistant never mistakes it for
18
+ * whatever it remembers of an older one.
19
+ */
20
+ export declare function docsTools(docs?: Doc[]): ToolDefinition[];
@@ -0,0 +1,106 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { GUIDELINES, loadDocs, searchDocs } from './docs.js';
3
+ export const SDK_VERSION = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version;
4
+ /**
5
+ * Docs and guidelines for the SDK version this server was installed with.
6
+ * Every answer names that version, so an assistant never mistakes it for
7
+ * whatever it remembers of an older one.
8
+ */
9
+ export function docsTools(docs = loadDocs()) {
10
+ const byName = new Map(docs.map((doc) => [doc.name, doc]));
11
+ const header = `zipRACK docs for @ziprack/sdk ${SDK_VERSION}`;
12
+ const unknownDoc = (name) => ({
13
+ isError: true,
14
+ error: `Unknown doc [${name}].`,
15
+ sdk_version: SDK_VERSION,
16
+ docs: docs.map((doc) => doc.name),
17
+ });
18
+ return [
19
+ {
20
+ name: 'ziprack_guidelines',
21
+ description: 'The rules every zipRACK applet must follow: mental model, package layout, manifest, SDK usage, runtime constraints and the pre-upload checklist. Read this before writing or changing applet code.',
22
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
23
+ run: () => {
24
+ const guidelines = byName.get(GUIDELINES);
25
+ return guidelines === undefined ? unknownDoc(GUIDELINES) : `${header}\n\n${guidelines.text}`;
26
+ },
27
+ },
28
+ {
29
+ name: 'ziprack_list_docs',
30
+ description: 'Every zipRACK doc in reading order, with its section anchors. Use the names with ziprack_read_doc.',
31
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
32
+ run: () => ({
33
+ sdk_version: SDK_VERSION,
34
+ docs: docs.map((doc) => ({
35
+ name: doc.name,
36
+ title: doc.title,
37
+ sections: doc.sections
38
+ .filter((section) => section.anchor !== null)
39
+ .map((section) => ({ anchor: section.anchor, heading: section.heading })),
40
+ })),
41
+ }),
42
+ },
43
+ {
44
+ name: 'ziprack_search_docs',
45
+ description: 'Search the zipRACK docs for the installed SDK version and return the best-matching sections in full. Pass several phrasings at once, e.g. ["setState resume", "attempt state"]. Search before guessing at an SDK method, manifest field or error code.',
46
+ inputSchema: {
47
+ type: 'object',
48
+ properties: {
49
+ queries: { type: 'array', items: { type: 'string' }, minItems: 1, description: 'Search phrases; a section scores on any of them.' },
50
+ limit: { type: 'integer', minimum: 1, maximum: 20, description: 'Sections to return. Defaults to 5.' },
51
+ },
52
+ required: ['queries'],
53
+ additionalProperties: false,
54
+ },
55
+ run: (args) => {
56
+ const queries = Array.isArray(args.queries) ? args.queries.filter((query) => typeof query === 'string') : [];
57
+ if (queries.length === 0) {
58
+ return { isError: true, error: 'queries must be a non-empty array of strings.' };
59
+ }
60
+ const limit = typeof args.limit === 'number' ? Math.min(Math.max(Math.trunc(args.limit), 1), 20) : 5;
61
+ const hits = searchDocs(docs, queries, limit);
62
+ if (hits.length === 0) {
63
+ return `${header}\n\nNo section matches ${JSON.stringify(queries)}. ziprack_list_docs shows what is covered.`;
64
+ }
65
+ return [header, ...hits.map(({ doc, section }) => {
66
+ const where = section.anchor === null ? doc.name : `${doc.name}#${section.anchor}`;
67
+ return `---\n[${where}] ${doc.title}\n\n${section.text}`;
68
+ })].join('\n\n');
69
+ },
70
+ },
71
+ {
72
+ name: 'ziprack_read_doc',
73
+ description: 'One zipRACK doc in full, or one section of it by anchor.',
74
+ inputSchema: {
75
+ type: 'object',
76
+ properties: {
77
+ doc: { type: 'string', description: 'A doc name from ziprack_list_docs, e.g. manifest.' },
78
+ section: { type: 'string', description: 'A section anchor, e.g. acks-and-errors.' },
79
+ },
80
+ required: ['doc'],
81
+ additionalProperties: false,
82
+ },
83
+ run: (args) => {
84
+ const name = String(args.doc ?? '').replace(/\.md$/, '');
85
+ const doc = byName.get(name);
86
+ if (doc === undefined) {
87
+ return unknownDoc(name);
88
+ }
89
+ if (typeof args.section !== 'string' || args.section === '') {
90
+ return `${header}\n\n${doc.text}`;
91
+ }
92
+ const anchor = args.section.replace(/^#/, '');
93
+ const section = doc.sections.find((candidate) => candidate.anchor === anchor);
94
+ if (section === undefined) {
95
+ return {
96
+ isError: true,
97
+ error: `Doc [${name}] has no section [${anchor}].`,
98
+ sdk_version: SDK_VERSION,
99
+ sections: doc.sections.flatMap((candidate) => candidate.anchor === null ? [] : [candidate.anchor]),
100
+ };
101
+ }
102
+ return `${header}\n\n${section.text}`;
103
+ },
104
+ },
105
+ ];
106
+ }