@ycadence/mcp 0.1.0 → 0.1.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.
Files changed (2) hide show
  1. package/dist/bin.js +354 -62
  2. package/package.json +2 -2
package/dist/bin.js CHANGED
@@ -20,129 +20,370 @@ function readEnv(name, env = processEnv()) {
20
20
  return;
21
21
  }
22
22
 
23
- // ../sdk/src/client.ts
23
+ // ../sdk/src/errors.ts
24
24
  class YCadenceError extends Error {
25
25
  status;
26
26
  code;
27
27
  issues;
28
- constructor(status, code, message, issues) {
28
+ requestId;
29
+ details;
30
+ retryAfter;
31
+ method;
32
+ path;
33
+ constructor(status, code, message, issues, extras = {}) {
29
34
  super(message);
30
35
  this.status = status;
31
36
  this.code = code;
32
37
  this.issues = issues;
33
38
  this.name = "YCadenceError";
39
+ this.requestId = extras.requestId;
40
+ this.details = extras.details;
41
+ this.retryAfter = extras.retryAfter;
42
+ this.method = extras.method;
43
+ this.path = extras.path;
34
44
  }
35
45
  get retryable() {
36
- return this.status === 429 || this.status >= 500;
46
+ return this.status === 0 || this.status === 408 || this.status === 429 || this.status >= 500;
37
47
  }
38
48
  }
39
49
 
50
+ class YCadenceValidationError extends YCadenceError {
51
+ name = "YCadenceValidationError";
52
+ }
53
+
54
+ class YCadencePermissionError extends YCadenceError {
55
+ name = "YCadencePermissionError";
56
+ }
57
+
58
+ class YCadenceNotFoundError extends YCadenceError {
59
+ name = "YCadenceNotFoundError";
60
+ }
61
+
62
+ class YCadenceConflictError extends YCadenceError {
63
+ name = "YCadenceConflictError";
64
+ }
65
+
66
+ class YCadencePreconditionError extends YCadenceError {
67
+ name = "YCadencePreconditionError";
68
+ }
69
+
70
+ class YCadenceRateLimitError extends YCadenceError {
71
+ name = "YCadenceRateLimitError";
72
+ }
73
+
74
+ class YCadenceNetworkError extends YCadenceError {
75
+ name = "YCadenceNetworkError";
76
+ }
77
+ function errorClassFor(status) {
78
+ if (status === 0 || status === 408)
79
+ return YCadenceNetworkError;
80
+ if (status === 400 || status === 422)
81
+ return YCadenceValidationError;
82
+ if (status === 401 || status === 403)
83
+ return YCadencePermissionError;
84
+ if (status === 404)
85
+ return YCadenceNotFoundError;
86
+ if (status === 409)
87
+ return YCadenceConflictError;
88
+ if (status === 412)
89
+ return YCadencePreconditionError;
90
+ if (status === 429)
91
+ return YCadenceRateLimitError;
92
+ return YCadenceError;
93
+ }
94
+ function createError(status, code, message, issues, extras = {}) {
95
+ const ErrorClass = errorClassFor(status);
96
+ return new ErrorClass(status, code, message, issues, extras);
97
+ }
98
+
99
+ class RequestIntercepted extends Error {
100
+ request;
101
+ name = "RequestIntercepted";
102
+ constructor(request) {
103
+ super(`${request.method} ${request.url} was not sent.`);
104
+ this.request = request;
105
+ }
106
+ }
107
+
108
+ // ../sdk/src/operations.ts
109
+ var OPERATIONS = {
110
+ health: { method: "GET", path: "/api/health" },
111
+ me: { method: "GET", path: "/api/v1/me" },
112
+ listProjects: { method: "GET", path: "/api/v1/projects" },
113
+ listWorkItems: { method: "GET", path: "/api/v1/projects/{projectId}/work-items" },
114
+ createWorkItem: { method: "POST", path: "/api/v1/projects/{projectId}/work-items" },
115
+ getWorkItem: { method: "GET", path: "/api/v1/work-items/{workItemId}" },
116
+ updateWorkItem: { method: "PATCH", path: "/api/v1/work-items/{workItemId}" },
117
+ deleteWorkItem: { method: "DELETE", path: "/api/v1/work-items/{workItemId}" },
118
+ listStates: { method: "GET", path: "/api/v1/projects/{projectId}/states" },
119
+ listLabels: { method: "GET", path: "/api/v1/projects/{projectId}/labels" },
120
+ createLabel: { method: "POST", path: "/api/v1/projects/{projectId}/labels" },
121
+ listTypes: { method: "GET", path: "/api/v1/projects/{projectId}/types" },
122
+ listCycles: { method: "GET", path: "/api/v1/projects/{projectId}/cycles" },
123
+ createCycle: { method: "POST", path: "/api/v1/projects/{projectId}/cycles" },
124
+ getCycleProgress: { method: "GET", path: "/api/v1/cycles/{cycleId}/progress" },
125
+ addWorkItemsToCycle: { method: "POST", path: "/api/v1/cycles/{cycleId}/work-items" },
126
+ listViews: { method: "GET", path: "/api/v1/projects/{projectId}/views" },
127
+ createView: { method: "POST", path: "/api/v1/projects/{projectId}/views" },
128
+ listIntake: { method: "GET", path: "/api/v1/projects/{projectId}/intake" },
129
+ submitIntake: { method: "POST", path: "/api/v1/projects/{projectId}/intake" },
130
+ triageIntake: { method: "POST", path: "/api/v1/intake-items/{intakeItemId}/triage" },
131
+ previewImport: { method: "POST", path: "/api/v1/projects/{projectId}/imports/preview" },
132
+ startImport: { method: "POST", path: "/api/v1/projects/{projectId}/imports" },
133
+ listImports: { method: "GET", path: "/api/v1/projects/{projectId}/imports" },
134
+ getImport: { method: "GET", path: "/api/v1/imports/{runId}" }
135
+ };
136
+ function isIdempotentMethod(method) {
137
+ return method === "GET" || method === "HEAD";
138
+ }
139
+ function buildPath(template, params = {}) {
140
+ return template.replace(/\{(\w+)\}/g, (_match, name) => {
141
+ const value = params[name];
142
+ if (value === undefined || value === "") {
143
+ throw new TypeError(`Missing path parameter "${name}" for ${template}.`);
144
+ }
145
+ return encodeURIComponent(value);
146
+ });
147
+ }
148
+
149
+ // ../sdk/src/pagination.ts
150
+ async function* paginate(fetchPage, options = {}) {
151
+ let cursor = options.cursor;
152
+ let yielded = 0;
153
+ do {
154
+ const page = await fetchPage(cursor);
155
+ for (const item of page.items) {
156
+ if (options.maxItems !== undefined && yielded >= options.maxItems)
157
+ return;
158
+ yielded += 1;
159
+ yield item;
160
+ }
161
+ cursor = page.nextCursor ?? undefined;
162
+ } while (cursor !== undefined);
163
+ }
164
+
165
+ // ../sdk/src/version.ts
166
+ var SDK_VERSION = "0.2.0";
167
+
168
+ // ../sdk/src/client.ts
169
+ var DEFAULT_RETRY = { retries: 2, baseDelayMs: 250, maxDelayMs: 8000 };
170
+
40
171
  class YCadenceClient {
172
+ options;
41
173
  baseUrl;
42
174
  apiKey;
43
175
  identityToken;
44
176
  timeoutMs;
45
177
  doFetch;
178
+ retry;
179
+ userAgent;
180
+ onResponse;
181
+ sleep;
46
182
  constructor(options) {
183
+ this.options = options;
47
184
  this.baseUrl = options.baseUrl.replace(/\/+$/, "");
48
185
  this.apiKey = options.apiKey;
49
186
  this.identityToken = options.identityToken;
50
187
  this.timeoutMs = options.timeoutMs ?? 30000;
51
- this.doFetch = options.fetch ?? globalThis.fetch;
188
+ this.doFetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
189
+ this.retry = { ...DEFAULT_RETRY, ...options.retry };
190
+ this.userAgent = [options.userAgent, `ycadence-sdk/${SDK_VERSION}`].filter((part) => part !== undefined && part !== "").join(" ");
191
+ this.onResponse = options.onResponse;
192
+ this.sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
193
+ }
194
+ withOptions(overrides) {
195
+ return new YCadenceClient({ ...this.options, ...overrides });
196
+ }
197
+ get fetchImplementation() {
198
+ return this.doFetch;
52
199
  }
53
200
  async health() {
54
- return this.request("GET", "/api/health");
55
- }
56
- async listWorkItems(projectId, options = {}) {
57
- const query = new URLSearchParams;
58
- if (options.filter !== undefined)
59
- query.set("filter", JSON.stringify(options.filter));
60
- if (options.limit !== undefined)
61
- query.set("limit", String(options.limit));
62
- if (options.cursor !== undefined)
63
- query.set("cursor", options.cursor);
64
- return this.request("GET", `/api/v1/projects/${projectId}/work-items?${query}`);
65
- }
66
- async* iterateWorkItems(projectId, options = {}) {
67
- let cursor;
68
- do {
69
- const page = await this.listWorkItems(projectId, {
70
- filter: options.filter,
71
- limit: options.pageSize ?? 100,
72
- cursor
73
- });
74
- for (const item of page.items)
75
- yield item;
76
- cursor = page.nextCursor ?? undefined;
77
- } while (cursor !== undefined);
201
+ return this.call("health");
202
+ }
203
+ async me(options) {
204
+ return this.call("me", {}, undefined, undefined, options);
205
+ }
206
+ async listProjects(query = {}, options) {
207
+ return this.call("listProjects", {}, query, undefined, options);
208
+ }
209
+ async listWorkItems(projectId, query = {}, options) {
210
+ return this.call("listWorkItems", { projectId }, {
211
+ filter: query.filter === undefined ? undefined : JSON.stringify(query.filter),
212
+ limit: query.limit,
213
+ cursor: query.cursor,
214
+ state: query.state,
215
+ priority: query.priority
216
+ }, undefined, options);
217
+ }
218
+ iterateWorkItems(projectId, options = {}) {
219
+ return paginate((cursor) => this.listWorkItems(projectId, {
220
+ filter: options.filter,
221
+ limit: options.pageSize ?? 100,
222
+ cursor
223
+ }));
224
+ }
225
+ async createWorkItem(projectId, body, options) {
226
+ return this.call("createWorkItem", { projectId }, undefined, body, options);
227
+ }
228
+ async getWorkItem(workItemId, options) {
229
+ return this.call("getWorkItem", { workItemId }, undefined, undefined, options);
230
+ }
231
+ async updateWorkItem(workItemId, body, options) {
232
+ return this.call("updateWorkItem", { workItemId }, undefined, body, options);
233
+ }
234
+ async deleteWorkItem(workItemId, options) {
235
+ await this.call("deleteWorkItem", { workItemId }, undefined, undefined, options);
78
236
  }
79
- async createWorkItem(projectId, body) {
80
- return this.request("POST", `/api/v1/projects/${projectId}/work-items`, body);
237
+ async listStates(projectId, options) {
238
+ return this.call("listStates", { projectId }, undefined, undefined, options);
81
239
  }
82
- async getWorkItem(workItemId) {
83
- return this.request("GET", `/api/v1/work-items/${workItemId}`);
240
+ async listLabels(projectId, options) {
241
+ return this.call("listLabels", { projectId }, undefined, undefined, options);
84
242
  }
85
- async updateWorkItem(workItemId, body) {
86
- return this.request("PATCH", `/api/v1/work-items/${workItemId}`, body);
243
+ async createLabel(projectId, body, options) {
244
+ return this.call("createLabel", { projectId }, undefined, body, options);
87
245
  }
88
- async deleteWorkItem(workItemId) {
89
- await this.request("DELETE", `/api/v1/work-items/${workItemId}`);
246
+ async listTypes(projectId, options) {
247
+ return this.call("listTypes", { projectId }, undefined, undefined, options);
90
248
  }
91
- async listStates(projectId) {
92
- return this.request("GET", `/api/v1/projects/${projectId}/states`);
249
+ async listCycles(projectId, options) {
250
+ return this.call("listCycles", { projectId }, undefined, undefined, options);
93
251
  }
94
- async listLabels(projectId) {
95
- return this.request("GET", `/api/v1/projects/${projectId}/labels`);
252
+ async createCycle(projectId, body, options) {
253
+ return this.call("createCycle", { projectId }, undefined, body, options);
96
254
  }
97
- async listCycles(projectId) {
98
- return this.request("GET", `/api/v1/projects/${projectId}/cycles`);
255
+ async getCycleProgress(cycleId, options) {
256
+ return this.call("getCycleProgress", { cycleId }, undefined, undefined, options);
99
257
  }
100
- async previewImport(projectId, body) {
101
- return this.request("POST", `/api/v1/projects/${projectId}/imports/preview`, body);
258
+ async addWorkItemsToCycle(cycleId, workItemIds, options) {
259
+ return this.call("addWorkItemsToCycle", { cycleId }, undefined, { workItemIds }, options);
102
260
  }
103
- async startImport(projectId, body) {
104
- return this.request("POST", `/api/v1/projects/${projectId}/imports`, body);
261
+ async listViews(projectId, options) {
262
+ return this.call("listViews", { projectId }, undefined, undefined, options);
105
263
  }
106
- async getImport(runId) {
107
- return this.request("GET", `/api/v1/imports/${runId}`);
264
+ async createView(projectId, body, options) {
265
+ return this.call("createView", { projectId }, undefined, body, options);
108
266
  }
109
- async listImports(projectId) {
110
- return this.request("GET", `/api/v1/projects/${projectId}/imports`);
267
+ async listIntake(projectId, query = {}, options) {
268
+ return this.call("listIntake", { projectId }, query, undefined, options);
111
269
  }
112
- async listViews(projectId) {
113
- return this.request("GET", `/api/v1/projects/${projectId}/views`);
270
+ async submitIntake(projectId, body, options) {
271
+ return this.call("submitIntake", { projectId }, undefined, body, options);
114
272
  }
115
- async request(method, path, body) {
273
+ async triageIntake(intakeItemId, body, options) {
274
+ return this.call("triageIntake", { intakeItemId }, undefined, body, options);
275
+ }
276
+ async previewImport(projectId, body, options) {
277
+ return this.call("previewImport", { projectId }, undefined, body, options);
278
+ }
279
+ async startImport(projectId, body, options) {
280
+ return this.call("startImport", { projectId }, undefined, body, options);
281
+ }
282
+ async getImport(runId, options) {
283
+ return this.call("getImport", { runId }, undefined, undefined, options);
284
+ }
285
+ async listImports(projectId, options) {
286
+ return this.call("listImports", { projectId }, undefined, undefined, options);
287
+ }
288
+ async call(operation, params = {}, query, body, options = {}) {
289
+ const { method, path } = OPERATIONS[operation];
290
+ let url = buildPath(path, params);
291
+ if (query !== undefined) {
292
+ const search = new URLSearchParams;
293
+ for (const [name, value] of Object.entries(query)) {
294
+ if (value !== undefined)
295
+ search.set(name, String(value));
296
+ }
297
+ const encoded = search.toString();
298
+ if (encoded.length > 0)
299
+ url += `?${encoded}`;
300
+ }
301
+ return this.request(method, url, body, options);
302
+ }
303
+ async request(method, path, body, options) {
304
+ const retryable = isIdempotentMethod(method) || options.idempotencyKey !== undefined;
305
+ const retries = retryable ? options.retries ?? this.retry.retries : 0;
306
+ for (let attempt = 0;; attempt += 1) {
307
+ try {
308
+ return await this.send(method, path, body, options, attempt);
309
+ } catch (error) {
310
+ const failure = error;
311
+ const again = attempt < retries && typeof failure.retryable === "boolean" && (failure.retryable || failure.code === "request_in_progress");
312
+ if (!again)
313
+ throw error;
314
+ await this.sleep(this.backoff(attempt, failure.retryAfter));
315
+ }
316
+ }
317
+ }
318
+ backoff(attempt, retryAfter) {
319
+ if (retryAfter !== undefined)
320
+ return Math.min(retryAfter * 1000, this.retry.maxDelayMs);
321
+ const exponential = this.retry.baseDelayMs * 2 ** attempt;
322
+ const jittered = exponential / 2 + Math.random() * (exponential / 2);
323
+ return Math.min(jittered, this.retry.maxDelayMs);
324
+ }
325
+ async send(method, path, body, options, attempt) {
116
326
  const controller = new AbortController;
117
327
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
328
+ const abort = () => controller.abort();
329
+ options.signal?.addEventListener("abort", abort);
330
+ const started = Date.now();
118
331
  let response;
119
332
  try {
120
333
  const identityToken = typeof this.identityToken === "function" ? await this.identityToken() : this.identityToken;
334
+ const headers = {
335
+ "x-api-key": this.apiKey,
336
+ ...identityToken === undefined || identityToken.length === 0 ? {} : { Authorization: `Bearer ${identityToken}` },
337
+ ...body === undefined ? {} : { "Content-Type": "application/json" },
338
+ Accept: "application/json",
339
+ "User-Agent": this.userAgent
340
+ };
341
+ if (options.idempotencyKey !== undefined && !isIdempotentMethod(method)) {
342
+ headers["Idempotency-Key"] = options.idempotencyKey;
343
+ }
344
+ if (options.expectedVersion !== undefined && !isIdempotentMethod(method)) {
345
+ headers["If-Match"] = `"${String(options.expectedVersion).replace(/"/g, "")}"`;
346
+ }
121
347
  response = await this.doFetch(`${this.baseUrl}${path}`, {
122
348
  method,
123
- headers: {
124
- "x-api-key": this.apiKey,
125
- ...identityToken === undefined || identityToken.length === 0 ? {} : { Authorization: `Bearer ${identityToken}` },
126
- ...body === undefined ? {} : { "Content-Type": "application/json" }
127
- },
349
+ headers,
128
350
  body: body === undefined ? undefined : JSON.stringify(body),
129
351
  signal: controller.signal
130
352
  });
131
353
  } catch (error) {
354
+ if (error instanceof RequestIntercepted)
355
+ throw error;
132
356
  if (error instanceof Error && error.name === "AbortError") {
133
- throw new YCadenceError(408, "timeout", `${method} ${path} timed out after ${this.timeoutMs}ms.`);
357
+ throw createError(408, "timeout", `${method} ${path} timed out after ${this.timeoutMs}ms.`, undefined, { method, path });
134
358
  }
135
- throw new YCadenceError(0, "network_error", `Could not reach ${this.baseUrl}: ${error instanceof Error ? error.message : "unknown error"}`);
359
+ throw createError(0, "network_error", `Could not reach ${this.baseUrl}: ${error instanceof Error ? error.message : "unknown error"}`, undefined, { method, path });
136
360
  } finally {
137
361
  clearTimeout(timer);
362
+ options.signal?.removeEventListener("abort", abort);
138
363
  }
364
+ const headerRequestId = response.headers?.get?.("x-request-id") ?? undefined;
365
+ this.onResponse?.({
366
+ method,
367
+ path,
368
+ status: response.status,
369
+ requestId: headerRequestId,
370
+ durationMs: Date.now() - started,
371
+ attempt,
372
+ replayed: response.headers?.get?.("idempotent-replayed") === "true"
373
+ });
139
374
  if (response.status === 204)
140
375
  return;
141
376
  const text = await response.text();
142
377
  const parsed = text.length === 0 ? undefined : safeJson(text);
143
378
  if (!response.ok) {
144
- const failure = parsed ?? {};
145
- throw new YCadenceError(response.status, failure.code ?? "unknown", failure.error ?? `${method} ${path} failed with ${response.status}.`, failure.issues);
379
+ const failure = isRecord(parsed) ? parsed : {};
380
+ throw createError(response.status, typeof failure.code === "string" ? failure.code : codeForStatus(response.status), typeof failure.error === "string" ? failure.error : `${method} ${path} failed with ${response.status}.`, failure.issues, {
381
+ requestId: failure.requestId ?? headerRequestId,
382
+ details: failure.details,
383
+ retryAfter: parseRetryAfter(response.headers?.get?.("retry-after") ?? null),
384
+ method,
385
+ path
386
+ });
146
387
  }
147
388
  return parsed;
148
389
  }
@@ -154,6 +395,57 @@ function safeJson(text) {
154
395
  return;
155
396
  }
156
397
  }
398
+ function isRecord(value) {
399
+ return typeof value === "object" && value !== null && !Array.isArray(value);
400
+ }
401
+ function codeForStatus(status) {
402
+ const codes = {
403
+ 400: "bad_request",
404
+ 401: "unauthorized",
405
+ 403: "forbidden",
406
+ 404: "not_found",
407
+ 409: "conflict",
408
+ 412: "precondition_failed",
409
+ 422: "validation_failed",
410
+ 429: "rate_limited"
411
+ };
412
+ return codes[status] ?? (status >= 500 ? "server_error" : "unknown");
413
+ }
414
+ function parseRetryAfter(value, now = Date.now()) {
415
+ if (value === null || value.trim() === "")
416
+ return;
417
+ const seconds = Number(value);
418
+ if (Number.isFinite(seconds))
419
+ return Math.max(0, seconds);
420
+ const date = Date.parse(value);
421
+ if (Number.isNaN(date))
422
+ return;
423
+ return Math.max(0, Math.ceil((date - now) / 1000));
424
+ }
425
+ // ../sdk/src/types.ts
426
+ var WORK_ITEM_FIELDS = {
427
+ id: true,
428
+ key: true,
429
+ sequenceId: true,
430
+ name: true,
431
+ priority: true,
432
+ stateId: true,
433
+ assigneeIds: true,
434
+ labelIds: true,
435
+ parentId: true,
436
+ sortOrder: true,
437
+ createdAt: true,
438
+ projectId: true,
439
+ version: true
440
+ };
441
+ var WORK_ITEM_DETAIL_FIELDS = {
442
+ ...WORK_ITEM_FIELDS,
443
+ description: true,
444
+ startDate: true,
445
+ targetDate: true,
446
+ archivedAt: true,
447
+ updatedAt: true
448
+ };
157
449
  // src/tools.ts
158
450
  var projectId = {
159
451
  type: "string",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycadence/mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Model Context Protocol server for YCadence: lets an AI agent read and file work items.",
5
5
  "keywords": [
6
6
  "ycadence",
@@ -40,7 +40,7 @@
40
40
  "provenance": false
41
41
  },
42
42
  "scripts": {
43
- "build": "rm -rf dist && bun build src/bin.ts --outdir dist --target node --format esm --conditions ycadence-source",
43
+ "build": "rm -rf dist && bun build src/bin.ts --outdir dist --target node --format esm --conditions ycadence-source --define __YCADENCE_SDK_VERSION__=\\\"$(node -p \"require('../sdk/package.json').version\")\\\"",
44
44
  "prepack": "cp ../../LICENSE LICENSE && bun run build",
45
45
  "type-check": "tsc --noEmit",
46
46
  "test": "vitest run"