@typeship-ax/mcp 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +43 -0
  3. package/api.json +5163 -0
  4. package/api.md +512 -0
  5. package/dist/core/http.d.ts +303 -0
  6. package/dist/core/http.d.ts.map +1 -0
  7. package/dist/core/http.js +770 -0
  8. package/dist/core/pagination.d.ts +51 -0
  9. package/dist/core/pagination.d.ts.map +1 -0
  10. package/dist/core/pagination.js +154 -0
  11. package/dist/dates.d.ts +33 -0
  12. package/dist/dates.d.ts.map +1 -0
  13. package/dist/dates.js +136 -0
  14. package/dist/errors.d.ts +81 -0
  15. package/dist/errors.d.ts.map +1 -0
  16. package/dist/errors.js +103 -0
  17. package/dist/index.d.ts +92 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +86 -0
  20. package/dist/mcp-protocol.d.ts +453 -0
  21. package/dist/mcp-protocol.d.ts.map +1 -0
  22. package/dist/mcp-protocol.js +1262 -0
  23. package/dist/mcp.d.ts +5 -0
  24. package/dist/mcp.d.ts.map +1 -0
  25. package/dist/mcp.js +449 -0
  26. package/dist/ops.d.ts +115 -0
  27. package/dist/ops.d.ts.map +1 -0
  28. package/dist/ops.js +79 -0
  29. package/dist/resources/account.d.ts +18 -0
  30. package/dist/resources/account.d.ts.map +1 -0
  31. package/dist/resources/account.js +26 -0
  32. package/dist/resources/api-keys.d.ts +37 -0
  33. package/dist/resources/api-keys.d.ts.map +1 -0
  34. package/dist/resources/api-keys.js +67 -0
  35. package/dist/resources/generate.d.ts +25 -0
  36. package/dist/resources/generate.d.ts.map +1 -0
  37. package/dist/resources/generate.js +41 -0
  38. package/dist/resources/generations.d.ts +31 -0
  39. package/dist/resources/generations.d.ts.map +1 -0
  40. package/dist/resources/generations.js +56 -0
  41. package/dist/resources/projects.d.ts +110 -0
  42. package/dist/resources/projects.d.ts.map +1 -0
  43. package/dist/resources/projects.js +220 -0
  44. package/dist/resources/spec-revisions.d.ts +47 -0
  45. package/dist/resources/spec-revisions.d.ts.map +1 -0
  46. package/dist/resources/spec-revisions.js +90 -0
  47. package/dist/schemas.d.ts +6 -0
  48. package/dist/schemas.d.ts.map +1 -0
  49. package/dist/schemas.js +88 -0
  50. package/dist/types.d.ts +759 -0
  51. package/dist/types.d.ts.map +1 -0
  52. package/dist/types.js +37 -0
  53. package/dist/worker.d.ts +5 -0
  54. package/dist/worker.d.ts.map +1 -0
  55. package/dist/worker.js +12 -0
  56. package/package.json +45 -0
  57. package/src/core/http.ts +1008 -0
  58. package/src/core/pagination.ts +195 -0
  59. package/src/dates.ts +126 -0
  60. package/src/errors.ts +117 -0
  61. package/src/index.ts +153 -0
  62. package/src/mcp-protocol.ts +1451 -0
  63. package/src/mcp.ts +448 -0
  64. package/src/ops.ts +174 -0
  65. package/src/resources/account.ts +43 -0
  66. package/src/resources/api-keys.ts +105 -0
  67. package/src/resources/generate.ts +69 -0
  68. package/src/resources/generations.ts +100 -0
  69. package/src/resources/projects.ts +391 -0
  70. package/src/resources/spec-revisions.ts +150 -0
  71. package/src/schemas.ts +90 -0
  72. package/src/types.ts +825 -0
  73. package/src/worker.ts +13 -0
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ /** Fetch-style handler: mount in any runtime (workers, serverless, node).
3
+ * Never rejects: every failure is an HTTP or JSON-RPC error response. */
4
+ export declare function handleHttp(request: Request): Promise<Response>;
5
+ //# sourceMappingURL=mcp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":";AA2SA;yEACyE;AACzE,wBAAsB,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAsDpE"}
package/dist/mcp.js ADDED
@@ -0,0 +1,449 @@
1
+ #!/usr/bin/env node
2
+ // typeship — MCP server. Generated by typeship — https://typeship.dev
3
+ // Speaks MCP 2026-07-28 (stateless: per-request _meta, server/discover; no
4
+ // initialize handshake, no sessions); the protocol layer and tool surface
5
+ // live in ./mcp-protocol.js, shared with typeship's hosted endpoint. Two
6
+ // transports, zero dependencies:
7
+ // node mcp.js stdio (newline-delimited JSON-RPC 2.0)
8
+ // node mcp.js --http Streamable HTTP on PORT (default 3000)
9
+ // Surface switches (flags or environment):
10
+ // --read-only / TYPESHIP_MCP_READ_ONLY=1 reads only; writes are not callable
11
+ // --tools a,b / TYPESHIP_MCP_TOOLS=a,b only these resources or tools
12
+ // TYPESHIP_MCP_MAX_RESULT_CHARS=<n> result size cap (default 64000)
13
+ // handleHttp() is exported for serverless/worker runtimes.
14
+ // In HTTP mode an incoming Authorization header is forwarded to the
15
+ // upstream API (per-request passthrough); stdio resolves auth from the
16
+ // environment, then from credentials/config saved by the CLI's `login`
17
+ // and `config` commands (same files, so one login covers both bins).
18
+ import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
19
+ import { homedir, tmpdir } from "node:os";
20
+ import { basename, join } from "node:path";
21
+ import { fileURLToPath } from "node:url";
22
+ import { TypeshipClient, formatDebugEvent } from "./index.js";
23
+ import { GLOBALS, OPS, buildArgs } from "./ops.js";
24
+ import { DEFAULT_MAX_RESULT_CHARS, SUPPORTED_PROTOCOL_VERSIONS, argumentsError, asJsonRpc, binaryOutcome, callSharedTool, checkRequestHeaders, dataOutcome, errorOutcome, fetchDocsText, handleRpc, isRpcOutcome, pageOutcome, parseIncludeList, prepareCall, serverInstructions, takeCancelled, textError, toolDefinitions, visibleOps, } from "./mcp-protocol.js";
25
+ const BIN = "typeship";
26
+ const PKG_NAME = "@typeship-ax/mcp";
27
+ const SERVER_NAME = "typeship-mcp";
28
+ const SERVER_VERSION = "0.6.0";
29
+ /** The MCP client's announced name (clientInfo in request _meta), for the User-Agent. */
30
+ let MCP_CLIENT_NAME = null;
31
+ function noteClientInfo(message) {
32
+ const meta = message?.params?._meta;
33
+ const name = meta?.["io.modelcontextprotocol/clientInfo"]?.name;
34
+ if (typeof name === "string" && name && name !== MCP_CLIENT_NAME) {
35
+ MCP_CLIENT_NAME = name.slice(0, 60);
36
+ clientInstance = undefined;
37
+ }
38
+ }
39
+ const DEFAULT_BASE_URL = "https://typeship.dev/api/v1";
40
+ const AUTH_SCALARS = [{ "option": "bearerToken", "flag": "token", "env": "TYPESHIP_TOKEN" }];
41
+ const BASIC = null;
42
+ const OAUTH_TOKEN_URL = null;
43
+ const ENVIRONMENTS = {};
44
+ const DOCS_URL_DEFAULT = "https://typeship.dev";
45
+ /** "meta" collapses per-operation tools into search/read/execute so huge
46
+ * APIs don't flood agent context with hundreds of tools. */
47
+ const TOOL_MODE = "meta";
48
+ /** Authorization server for OAuth discovery (RFC 9728), from the spec. */
49
+ const OAUTH_ISSUER = null;
50
+ /** Project-supplied guidance appended to the server instructions. */
51
+ const CUSTOM_INSTRUCTIONS = null;
52
+ /** The tool that returns the caller (the CLI's whoami target), named in the instructions. */
53
+ const IDENTITY_TOOL = "account_retrieve";
54
+ /** Surface switches: reads only, and/or a subset of resources or tools.
55
+ * Flags win over the environment. */
56
+ const ARGV = process.argv.slice(2);
57
+ const READ_ONLY = ARGV.includes("--read-only") || process.env["TYPESHIP_MCP_READ_ONLY"] === "1" || process.env["TYPESHIP_MCP_READ_ONLY"] === "true";
58
+ const INCLUDE = parseIncludeList(ARGV.includes("--tools") ? ARGV[ARGV.indexOf("--tools") + 1] : process.env["TYPESHIP_MCP_TOOLS"]);
59
+ const MAX_RESULT_CHARS = Number(process.env["TYPESHIP_MCP_MAX_RESULT_CHARS"]) || DEFAULT_MAX_RESULT_CHARS;
60
+ /** One sentence on where credentials come from on each transport; goes
61
+ * into the instructions and into 401 results. */
62
+ const AUTH_HINT_STDIO = "Credentials come from the MCP server's environment (TYPESHIP_TOKEN) or from 'typeship login'";
63
+ const AUTH_HINT_HTTP = "Send the API credential as the Authorization header of each MCP request; it is forwarded to the API as is";
64
+ /** The tool list is fixed at generation, so clients may cache it for an
65
+ * hour and shared caches may hold it (identical for every caller). */
66
+ const TOOLS_TTL_MS = 60 * 60 * 1000;
67
+ function configDir() {
68
+ return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), BIN);
69
+ }
70
+ function readJson(file) {
71
+ try {
72
+ return JSON.parse(readFileSync(join(configDir(), file), "utf8"));
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ }
78
+ function makeClient() {
79
+ const stored = readJson("credentials.json");
80
+ const config = readJson("config.json");
81
+ const options = {};
82
+ const baseUrl = process.env["TYPESHIP_BASE_URL"]
83
+ ?? config?.baseUrl
84
+ ?? (config?.environment !== undefined ? ENVIRONMENTS[config.environment] : undefined)
85
+ ?? DEFAULT_BASE_URL ?? undefined;
86
+ if (baseUrl === undefined) {
87
+ // Thrown, not exited: the agent gets an isError result naming the fix
88
+ // instead of a server that vanished mid-conversation.
89
+ throw new Error("No base URL configured: set TYPESHIP_BASE_URL in the MCP server's environment, or run '" + BIN + " config set base-url <url>'.");
90
+ }
91
+ options.baseUrl = baseUrl;
92
+ for (const a of AUTH_SCALARS) {
93
+ const v = process.env[a.env] ?? stored?.scalars?.[a.option];
94
+ if (v !== undefined)
95
+ options[a.option] = v;
96
+ }
97
+ if (BASIC) {
98
+ const username = process.env[BASIC.envUser] ?? stored?.basic?.username;
99
+ const password = process.env[BASIC.envPass] ?? stored?.basic?.password;
100
+ if (username !== undefined && password !== undefined)
101
+ options.basicAuth = { username, password };
102
+ }
103
+ const oauthClientId = process.env["TYPESHIP_CLIENT_ID"];
104
+ const oauthClientSecret = process.env["TYPESHIP_CLIENT_SECRET"];
105
+ if ((oauthClientId === undefined) !== (oauthClientSecret === undefined)) {
106
+ throw new Error("OAuth client credentials are incomplete: set both TYPESHIP_CLIENT_ID and TYPESHIP_CLIENT_SECRET in the MCP server's environment.");
107
+ }
108
+ if (oauthClientId !== undefined && oauthClientSecret !== undefined) {
109
+ const tokenUrl = process.env["TYPESHIP_TOKEN_URL"] ?? OAUTH_TOKEN_URL ?? undefined;
110
+ if (!tokenUrl)
111
+ throw new Error("OAuth client credentials need a token URL: set TYPESHIP_TOKEN_URL in the MCP server's environment.");
112
+ options.clientCredentials = { clientId: oauthClientId, clientSecret: oauthClientSecret, tokenUrl };
113
+ }
114
+ if (options.bearerToken === undefined && stored?.oauth?.accessToken !== undefined) {
115
+ options.bearerToken = stored.oauth.accessToken;
116
+ }
117
+ for (const g of GLOBALS) {
118
+ const value = process.env["TYPESHIP_" + g.envSuffix];
119
+ if (value !== undefined)
120
+ options[g.option] = value;
121
+ }
122
+ if (process.env["TYPESHIP_DEBUG"] === "1") {
123
+ options.debug = (event) => process.stderr.write(formatDebugEvent(BIN + "-mcp", event) + "\n");
124
+ }
125
+ // The local MCP server identifies itself (surface + the client it serves, when announced).
126
+ options.defaultHeaders = { "User-Agent": PKG_NAME + "-mcp/" + SERVER_VERSION + " (typeship" + (MCP_CLIENT_NAME ? "; client=" + MCP_CLIENT_NAME : "") + ")" };
127
+ return new TypeshipClient(options);
128
+ }
129
+ let clientInstance;
130
+ function getClient() {
131
+ return (clientInstance ??= makeClient());
132
+ }
133
+ /** Run one operation through the generated SDK: the same client, the same
134
+ * typed errors and pagination a hand-written caller would get. */
135
+ async function callOperation(op, rawArgs, authHeader) {
136
+ // Arguments are checked against the tool's schema first: unknown names,
137
+ // wrong types and missing requirements come back as one isError result,
138
+ // nothing reaches the API half-formed and nothing is dropped silently.
139
+ const prepared = prepareCall(op, rawArgs, { maxChars: MAX_RESULT_CHARS });
140
+ if (!prepared.ok)
141
+ return prepared.outcome;
142
+ const { args, fields, maxChars } = prepared.call;
143
+ const values = {};
144
+ for (const p of op.params) {
145
+ if (args[p.name] !== undefined)
146
+ values[p.name] = args[p.name];
147
+ }
148
+ // File arguments are local paths on this machine; unreadable ones are
149
+ // argument errors, reported before anything is sent.
150
+ const fileIssues = [];
151
+ let rawBody = op.bodyStyle === "data" ? args.body : undefined;
152
+ if (LOCAL_PROCESS) {
153
+ for (const p of op.params) {
154
+ if (p.type !== "file" || typeof values[p.name] !== "string")
155
+ continue;
156
+ try {
157
+ values[p.name] = fileArgument(values[p.name]);
158
+ }
159
+ catch (e) {
160
+ fileIssues.push({ code: "INVALID_ARGUMENT", argument: p.name, message: p.name + ": cannot read " + String(values[p.name]) + " (" + e.message + ")" });
161
+ }
162
+ }
163
+ if (op.bodyKind === "binary" && typeof rawBody === "string") {
164
+ try {
165
+ rawBody = fileArgument(rawBody);
166
+ }
167
+ catch (e) {
168
+ fileIssues.push({ code: "INVALID_ARGUMENT", argument: "body", message: "body: cannot read " + String(rawBody) + " (" + e.message + ")" });
169
+ }
170
+ }
171
+ }
172
+ if (fileIssues.length > 0)
173
+ return argumentsError(op, fileIssues);
174
+ const callArgs = buildArgs(op, values, rawBody, typeof args.select === "string" ? args.select : undefined);
175
+ if (authHeader)
176
+ callArgs.push({ headers: { Authorization: authHeader } });
177
+ const errorContext = {
178
+ authHint: authHeader !== undefined ? AUTH_HINT_HTTP : AUTH_HINT_STDIO,
179
+ docsUrl: docsSource.docsUrl(),
180
+ hadCredential: authHeader !== undefined || AUTH_SCALARS.some((a) => process.env[a.env] !== undefined) || readJson("credentials.json") !== null,
181
+ };
182
+ const shape = { fields, maxChars, pagination: op.pagination, args };
183
+ try {
184
+ const target = getClient()[op.resource];
185
+ const result = await target[op.method](...callArgs);
186
+ if (!result.ok)
187
+ return errorOutcome(result.error, errorContext);
188
+ if (op.paginated) {
189
+ const page = result.data;
190
+ return pageOutcome(page.items, page.nextPageParams(), shape);
191
+ }
192
+ // A binary body (the SDK hands back a Blob): an image block, or a file
193
+ // on disk, never "{}".
194
+ if (result.data instanceof Blob)
195
+ return binaryOutcome(result.data, { tool: op.tool, ...(LOCAL_PROCESS ? { saveBinary } : {}) });
196
+ return dataOutcome(result.data, shape);
197
+ }
198
+ catch (e) {
199
+ return textError(e.message ?? "Tool call failed");
200
+ }
201
+ }
202
+ /** Running as a process on this machine (stdio, or --http launched here),
203
+ * as opposed to imported by a worker: the server can read and write local
204
+ * files, so uploads are tools and binaries are saved to disk. */
205
+ const LOCAL_PROCESS = invokedDirectly();
206
+ /** The callable operations: event streams are CLI-only, uploads are tools
207
+ * only on a local server (mcpExposed), writes are out under --read-only,
208
+ * and --tools narrows to a subset. A hidden operation is unknown to
209
+ * tools/call and execute alike. */
210
+ const MCP_OPS = visibleOps(OPS, { readOnly: READ_ONLY, include: INCLUDE, uploads: LOCAL_PROCESS });
211
+ const HIDDEN_WRITES = READ_ONLY ? visibleOps(OPS, { include: INCLUDE, uploads: LOCAL_PROCESS }).length - MCP_OPS.length : 0;
212
+ const HAS_UPLOADS = MCP_OPS.some((op) => op.bodyKind === "multipart" || op.bodyKind === "binary");
213
+ /** A local file as an upload part (multipart field or raw body). */
214
+ function fileArgument(path) {
215
+ return new File([readFileSync(path)], basename(path));
216
+ }
217
+ /** Where binary responses land on a local server: a per-server temp dir. */
218
+ function saveBinary(bytes, _mediaType, suggestedName) {
219
+ const dir = join(tmpdir(), SERVER_NAME);
220
+ mkdirSync(dir, { recursive: true });
221
+ const path = join(dir, Date.now().toString(36) + "-" + suggestedName);
222
+ writeFileSync(path, bytes);
223
+ return path;
224
+ }
225
+ const docsSource = {
226
+ ops: MCP_OPS,
227
+ docsUrl: () => readJson("config.json")?.docsUrl ?? DOCS_URL_DEFAULT,
228
+ fetchText: fetchDocsText,
229
+ };
230
+ /** The MCP server for one request context (the HTTP transport passes the
231
+ * caller's Authorization through; stdio has none). */
232
+ function serverFor(authHeader) {
233
+ const website = docsSource.docsUrl();
234
+ return {
235
+ serverInfo: { name: SERVER_NAME, title: "typeship", version: SERVER_VERSION, ...(website ? { websiteUrl: website } : {}) },
236
+ instructions: serverInstructions({
237
+ title: "typeship",
238
+ toolCount: MCP_OPS.length,
239
+ mode: TOOL_MODE,
240
+ readOnly: READ_ONLY,
241
+ hiddenWrites: HIDDEN_WRITES,
242
+ authHint: authHeader !== undefined ? AUTH_HINT_HTTP : AUTH_HINT_STDIO,
243
+ identityTool: MCP_OPS.some((o) => o.tool === IDENTITY_TOOL) ? IDENTITY_TOOL : null,
244
+ uploads: HAS_UPLOADS,
245
+ custom: CUSTOM_INSTRUCTIONS,
246
+ }),
247
+ toolsTtlMs: TOOLS_TTL_MS,
248
+ listTools: () => toolDefinitions(docsSource.ops, TOOL_MODE),
249
+ callTool: async (name, args) => {
250
+ const shared = await callSharedTool(name, args, docsSource, (op, opArgs) => callOperation(op, opArgs, authHeader));
251
+ if (shared !== undefined)
252
+ return shared;
253
+ // tools/list is the callable contract. In meta mode every operation
254
+ // goes through execute, including its destructive-operation
255
+ // confirmation gate; an unlisted operation name must stay unknown.
256
+ if (TOOL_MODE !== "operations")
257
+ return undefined;
258
+ const op = MCP_OPS.find((o) => o.tool === name);
259
+ return op ? callOperation(op, args, authHeader) : undefined;
260
+ },
261
+ };
262
+ }
263
+ // ---- Streamable HTTP transport ---------------------------------------------
264
+ /** Origins allowed to call the HTTP transport from a browser. Non-browser
265
+ * MCP clients send no Origin and are always accepted; the spec requires
266
+ * validating the header when present (DNS-rebinding defense), so browser
267
+ * callers must be same-host, localhost, or listed in
268
+ * TYPESHIP_MCP_ALLOWED_ORIGINS (comma-separated, "*" for any). */
269
+ function originAllowed(request) {
270
+ const origin = request.headers.get("origin");
271
+ if (origin === null)
272
+ return true;
273
+ const allowed = (process.env["TYPESHIP_MCP_ALLOWED_ORIGINS"] ?? "").split(",").map((s) => s.trim()).filter(Boolean);
274
+ if (allowed.includes("*") || allowed.includes(origin))
275
+ return true;
276
+ try {
277
+ const o = new URL(origin);
278
+ if (o.host === new URL(request.url).host)
279
+ return true;
280
+ return ["localhost", "127.0.0.1", "[::1]"].includes(o.hostname);
281
+ }
282
+ catch {
283
+ return false;
284
+ }
285
+ }
286
+ function corsHeaders(request) {
287
+ return {
288
+ "Access-Control-Allow-Origin": request.headers.get("origin") ?? "*",
289
+ "Access-Control-Allow-Methods": "POST, OPTIONS",
290
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, Accept, MCP-Protocol-Version, Mcp-Method, Mcp-Name",
291
+ Vary: "Origin",
292
+ };
293
+ }
294
+ function protectedResourceMetadata(origin) {
295
+ return {
296
+ resource: origin,
297
+ ...(OAUTH_ISSUER ? { authorization_servers: [OAUTH_ISSUER] } : {}),
298
+ bearer_methods_supported: ["header"],
299
+ };
300
+ }
301
+ /** Fetch-style handler: mount in any runtime (workers, serverless, node).
302
+ * Never rejects: every failure is an HTTP or JSON-RPC error response. */
303
+ export async function handleHttp(request) {
304
+ const url = new URL(request.url);
305
+ const cors = corsHeaders(request);
306
+ const json = (status, body, extra = {}) => new Response(JSON.stringify(body), { status, headers: { ...cors, "Content-Type": "application/json", ...extra } });
307
+ if (request.method === "OPTIONS") {
308
+ return new Response(null, { status: originAllowed(request) ? 204 : 403, headers: cors });
309
+ }
310
+ if (!originAllowed(request)) {
311
+ return json(403, { jsonrpc: "2.0", error: { code: -32600, message: "Origin not allowed. Set TYPESHIP_MCP_ALLOWED_ORIGINS to permit browser callers." } });
312
+ }
313
+ if (url.pathname.endsWith("/.well-known/oauth-protected-resource")) {
314
+ return json(200, protectedResourceMetadata(url.origin));
315
+ }
316
+ if (request.method !== "POST") {
317
+ // GET streams, DELETE (session end) and Mcp-Session-Id belong to earlier
318
+ // revisions; the spec asks modern-only servers to answer 405.
319
+ return json(405, { error: "POST JSON-RPC messages to this endpoint (MCP " + SUPPORTED_PROTOCOL_VERSIONS[0] + ", stateless)." }, { Allow: "POST, OPTIONS" });
320
+ }
321
+ const authHeader = request.headers.get("authorization") ?? undefined;
322
+ const envHasAuth = AUTH_SCALARS.some((a) => process.env[a.env] !== undefined);
323
+ if (OAUTH_ISSUER && !authHeader && !envHasAuth) {
324
+ return json(401, { error: "Authorization required." }, {
325
+ "WWW-Authenticate": 'Bearer resource_metadata="' + url.origin + '/.well-known/oauth-protected-resource"',
326
+ });
327
+ }
328
+ let incoming;
329
+ try {
330
+ incoming = await request.json();
331
+ }
332
+ catch {
333
+ return json(400, { jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
334
+ }
335
+ const parsed = asJsonRpc(incoming);
336
+ if (isRpcOutcome(parsed))
337
+ return json(parsed.status, parsed.message);
338
+ // Header/body agreement is checked for requests; the spec defines no
339
+ // header requirements for notification POSTs.
340
+ if (parsed.id !== undefined && parsed.id !== null && parsed.method !== "initialize") {
341
+ const mismatch = checkRequestHeaders(request.headers, parsed);
342
+ if (mismatch)
343
+ return json(mismatch.status, mismatch.message);
344
+ }
345
+ noteClientInfo(parsed);
346
+ let outcome;
347
+ try {
348
+ outcome = await handleRpc(serverFor(authHeader), parsed);
349
+ }
350
+ catch (e) {
351
+ return json(500, { jsonrpc: "2.0", id: parsed.id ?? null, error: { code: -32603, message: e.message ?? "Internal error" } });
352
+ }
353
+ if (outcome.message === null) {
354
+ return new Response(null, { status: outcome.status, headers: cors });
355
+ }
356
+ return json(outcome.status, outcome.message);
357
+ }
358
+ function startHttp(port) {
359
+ void import("node:http").then(({ createServer }) => {
360
+ createServer((req, res) => {
361
+ let body = "";
362
+ req.on("data", (chunk) => (body += chunk));
363
+ req.on("end", () => {
364
+ void handleHttp(new Request("http://localhost:" + port + (req.url ?? "/"), {
365
+ method: req.method,
366
+ headers: Object.fromEntries(Object.entries(req.headers).map(([k, v]) => [k, Array.isArray(v) ? v.join(", ") : String(v ?? "")])),
367
+ ...(req.method === "POST" ? { body } : {}),
368
+ })).then(async (response) => {
369
+ res.statusCode = response.status;
370
+ response.headers.forEach((value, key) => res.setHeader(key, value));
371
+ res.end(await response.text());
372
+ }).catch((e) => {
373
+ // handleHttp never rejects; this guards the Request construction
374
+ // and socket writes so one bad connection can't take the server.
375
+ process.stderr.write(SERVER_NAME + ": " + (e.message ?? String(e)) + "\n");
376
+ if (!res.headersSent)
377
+ res.statusCode = 500;
378
+ res.end();
379
+ });
380
+ });
381
+ }).listen(port, () => {
382
+ process.stderr.write(SERVER_NAME + " listening on http://localhost:" + port + "\n");
383
+ });
384
+ });
385
+ }
386
+ // ---- stdio transport --------------------------------------------------------
387
+ async function startStdio() {
388
+ // dynamic: node:readline doesn't exist in worker runtimes, and workers
389
+ // never take the stdio path
390
+ const { createInterface } = await import("node:readline");
391
+ const rl = createInterface({ input: process.stdin });
392
+ const write = (message) => process.stdout.write(JSON.stringify(message) + "\n");
393
+ const server = serverFor();
394
+ rl.on("line", (line) => {
395
+ const trimmed = line.trim();
396
+ if (trimmed === "")
397
+ return;
398
+ let incoming;
399
+ try {
400
+ incoming = JSON.parse(trimmed);
401
+ }
402
+ catch {
403
+ write({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
404
+ return;
405
+ }
406
+ const id = incoming !== null && typeof incoming === "object" && !Array.isArray(incoming) ? incoming.id : undefined;
407
+ noteClientInfo(incoming);
408
+ void handleRpc(server, incoming)
409
+ .then((outcome) => {
410
+ if (outcome.message === null)
411
+ return;
412
+ // A cancelled request gets no response (spec: MUST NOT send further messages).
413
+ if (takeCancelled(id))
414
+ return;
415
+ write(outcome.message);
416
+ })
417
+ .catch((e) => {
418
+ if (id !== undefined) {
419
+ write({ jsonrpc: "2.0", id: id ?? null, error: { code: -32603, message: e.message ?? "Internal error" } });
420
+ }
421
+ });
422
+ });
423
+ // Spec: exit promptly when stdin closes.
424
+ rl.on("close", () => process.exit(0));
425
+ }
426
+ /** True when this file is the process entry: `node dist/mcp.js`, the
427
+ * package's `typeship-mcp` bin (a symlink, so compare real paths), or
428
+ * `npx tsx src/mcp.ts`. False when imported (worker.ts, tests). */
429
+ function invokedDirectly() {
430
+ const entry = process.argv[1];
431
+ if (typeof entry !== "string" || entry === "")
432
+ return false;
433
+ try {
434
+ return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url));
435
+ }
436
+ catch {
437
+ return /mcp\.(js|mjs|ts)$/.test(entry);
438
+ }
439
+ }
440
+ if (invokedDirectly()) {
441
+ const httpFlag = process.argv.indexOf("--http");
442
+ if (httpFlag !== -1) {
443
+ const port = Number(process.argv[httpFlag + 1]) || Number(process.env.PORT) || 3000;
444
+ startHttp(port);
445
+ }
446
+ else {
447
+ void startStdio();
448
+ }
449
+ }
package/dist/ops.d.ts ADDED
@@ -0,0 +1,115 @@
1
+ export interface ParamSpec {
2
+ /** wire name (JSON key, query name, or path placeholder) */
3
+ name: string;
4
+ /** CLI flag, without the leading dashes */
5
+ flag: string;
6
+ /** property name the SDK params object uses */
7
+ ts: string;
8
+ kind: "path" | "query" | "header" | "body";
9
+ /** "file": a binary multipart field; the CLI takes a path and uploads it.
10
+ * "array"/"object": the schema says so (a lone scalar flag value becomes a
11
+ * one-item array; an object must be JSON). "json": a union or untyped
12
+ * value — JSON if it parses, else the raw text. */
13
+ type: "string" | "number" | "boolean" | "array" | "object" | "json" | "file";
14
+ required: boolean;
15
+ enum?: string[];
16
+ /** Element type of an "array" param: what each repeated/comma value coerces to. */
17
+ items?: {
18
+ type: "string" | "number" | "boolean" | "object" | "json";
19
+ enum?: string[];
20
+ };
21
+ /** A date-shaped string: the CLI and MCP server resolve relative forms (-P7D, today). */
22
+ format?: "date" | "date-time";
23
+ description?: string;
24
+ /** Settable once on the client (globals option); optional per call. */
25
+ global?: boolean;
26
+ }
27
+ export interface OpSpec {
28
+ resource: string;
29
+ method: string;
30
+ command: [string, string];
31
+ /** Full kebab method name, still routable, when command[1] was shortened. */
32
+ commandAlias?: string;
33
+ tool: string;
34
+ httpMethod: string;
35
+ path: string;
36
+ summary?: string;
37
+ /** Full operation description, for the docs command (help stays terse). */
38
+ description?: string;
39
+ /** What agents see in tools/list: summary, the description's first
40
+ * sentence, method + path. Computed once so package and hosted agree. */
41
+ toolDescription: string;
42
+ paginated: boolean;
43
+ hasBody: boolean;
44
+ bodyStyle: "none" | "fields" | "data";
45
+ /** wire encoding of the body, for generic executors */
46
+ bodyKind: "json" | "form" | "multipart" | "binary" | "text" | null;
47
+ /** Success body is a text/event-stream: the CLI streams events as NDJSON;
48
+ * not an MCP tool. */
49
+ sse: boolean;
50
+ hasParams: boolean;
51
+ /** GraphQL ops whose field returns an object accept a selection-set
52
+ * override (a leaf-returning field has nothing to select). */
53
+ select: boolean;
54
+ /** present for GraphQL ops: enough to build the request document */
55
+ graphql?: {
56
+ field: string;
57
+ kind: "query" | "mutation";
58
+ docPrefix: string;
59
+ defaultSelection: string;
60
+ };
61
+ /** Retried by default (idempotent verb), like the SDK method. */
62
+ idempotent: boolean;
63
+ /** Header the SDK fills with one UUID per call (Idempotency-Key). */
64
+ idempotencyHeader?: string;
65
+ /** Operation-level retry policy (x-typeship-retries). */
66
+ retry?: {
67
+ maxRetries?: number;
68
+ statuses?: number[];
69
+ initialDelayMs?: number;
70
+ maxDelayMs?: number;
71
+ retryNonIdempotent?: boolean;
72
+ };
73
+ /** Page-walking config for generic executors, same shape as the SDK's PageConfig. */
74
+ pagination?: {
75
+ style: "cursor" | "cursorFromLastId" | "page" | "offset";
76
+ itemsField: string;
77
+ cursorParam?: string;
78
+ nextCursorField?: string;
79
+ hasMoreField?: string;
80
+ idField?: string;
81
+ pageParam?: string;
82
+ offsetParam?: string;
83
+ limitParam?: string;
84
+ };
85
+ /** The spec's security requirement: "required" fails fast without a
86
+ * credential; "none" works anonymously; "optional" is either. */
87
+ auth: "required" | "optional" | "none";
88
+ /** Whether calling this operation only reads, writes, or destroys state. */
89
+ safety: "read" | "write" | "destructive";
90
+ params: ParamSpec[];
91
+ inputSchema: Record<string, unknown>;
92
+ /** Deterministic arguments derived from inputSchema for runnable examples. */
93
+ exampleArguments: Record<string, unknown>;
94
+ /** What the tool returns (JSON Schema 2020-12), when the spec documents
95
+ * a success body; paginated tools describe { items, hasMore, nextPage }. */
96
+ outputSchema?: Record<string, unknown>;
97
+ }
98
+ export declare const OPS: OpSpec[];
99
+ /** Parameters settable once on the client (globals option): CLI/MCP fill
100
+ * them from flags or environment; the SDK falls back per request. */
101
+ export declare const GLOBALS: {
102
+ name: string;
103
+ option: string;
104
+ flag: string;
105
+ envSuffix: string;
106
+ }[];
107
+ export declare function findOp(resource: string, method: string): OpSpec | undefined;
108
+ export declare function missingRequired(op: OpSpec, values: Record<string, unknown>): string[];
109
+ /**
110
+ * Assemble the SDK method's positional arguments from a flat map of values
111
+ * keyed by wire name (plus an optional raw body for bodyStyle "data" or a
112
+ * --data base object).
113
+ */
114
+ export declare function buildArgs(op: OpSpec, values: Record<string, unknown>, dataBody?: unknown, select?: string): unknown[];
115
+ //# sourceMappingURL=ops.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ops.d.ts","sourceRoot":"","sources":["../src/ops.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,SAAS;IACxB,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAC;IACb,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC;IAC3C;;;uDAGmD;IACnD,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;IAC7E,QAAQ,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,mFAAmF;IACnF,KAAK,CAAC,EAAE;QAAE,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IACvF,yFAAyF;IACzF,MAAM,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uEAAuE;IACvE,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,MAAM;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1B,6EAA6E;IAC7E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2EAA2E;IAC3E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;6EACyE;IACzE,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;IACtC,uDAAuD;IACvD,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,WAAW,GAAG,QAAQ,GAAG,MAAM,GAAG,IAAI,CAAC;IACnE;0BACsB;IACtB,GAAG,EAAE,OAAO,CAAC;IACb,SAAS,EAAE,OAAO,CAAC;IACnB;kEAC8D;IAC9D,MAAM,EAAE,OAAO,CAAC;IAChB,oEAAoE;IACpE,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,GAAG,UAAU,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAA;KAAE,CAAC;IACrG,iEAAiE;IACjE,UAAU,EAAE,OAAO,CAAC;IACpB,qEAAqE;IACrE,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,yDAAyD;IACzD,KAAK,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,kBAAkB,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IACjI,qFAAqF;IACrF,UAAU,CAAC,EAAE;QACX,KAAK,EAAE,QAAQ,GAAG,kBAAkB,GAAG,MAAM,GAAG,QAAQ,CAAC;QACzD,UAAU,EAAE,MAAM,CAAC;QACnB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;IACF;qEACiE;IACjE,IAAI,EAAE,UAAU,GAAG,UAAU,GAAG,MAAM,CAAC;IACvC,4EAA4E;IAC5E,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,aAAa,CAAC;IACzC,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,8EAA8E;IAC9E,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC1C;gFAC4E;IAC5E,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC;AAED,eAAO,MAAM,GAAG,EAAE,MAAM,EAkBvB,CAAC;AAEF;qEACqE;AACrE,eAAO,MAAM,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,EAEtF,CAAC;AAEF,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE3E;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,CAIrF;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,QAAQ,CAAC,EAAE,OAAO,EAClB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,EAAE,CAuCX"}