contextforge-mcp 0.1.73 → 0.1.75

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/dist/index.js CHANGED
@@ -3,7 +3,8 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
5
  import { ApiClient, ApiClientError, readProjectLinkConfig, } from "./api-client.js";
6
- import { ConfigSchema, IngestInputSchema, QueryInputSchema, CreateProjectInputSchema, CreateSpaceInputSchema, RelateInputSchema, ListRelationshipsInputSchema, DeleteInputSchema, GitConnectInputSchema, GitActivateInputSchema, GitDisconnectInputSchema, GitSyncInputSchema, GitHistoryInputSchema, SnapshotCreateInputSchema, SnapshotRestoreInputSchema, SnapshotDeleteInputSchema, ExportInputSchema, ImportInputSchema, IngestBatchInputSchema, DeleteBatchInputSchema, LinkProjectInputSchema, parseArrayInput, } from "./types.js";
6
+ import { ConfigSchema, IngestInputSchema, QueryInputSchema, CreateProjectInputSchema, CreateSpaceInputSchema, RelateInputSchema, ListRelationshipsInputSchema, DeleteInputSchema, GitConnectInputSchema, GitActivateInputSchema, GitDisconnectInputSchema, GitSyncInputSchema, GitHistoryInputSchema, SnapshotCreateInputSchema, SnapshotRestoreInputSchema, SnapshotDeleteInputSchema, ExportInputSchema, ImportInputSchema, IngestBatchInputSchema, DeleteBatchInputSchema, LinkProjectInputSchema, SkillsListInputSchema, SkillsGetInputSchema, SkillsCreateInputSchema, SkillsUpdateInputSchema, SkillsDeleteInputSchema, SkillsRunInputSchema, parseArrayInput, } from "./types.js";
7
+ import { resolveTaskIdentifier } from "./task-params.js";
7
8
  import { appendFileSync } from "fs";
8
9
  import { createRequire } from "module";
9
10
  import { checkForUpdates, getUpdateNotice } from "./update-checker.js";
@@ -88,6 +89,12 @@ function logTool(toolName, details) {
88
89
  tasks_add_comment: "💬",
89
90
  collaborators_list: "👥",
90
91
  project_share: "🔗",
92
+ skills_list: "🧰",
93
+ skills_get: "🔧",
94
+ skills_create: "➕",
95
+ skills_update: "✏️",
96
+ skills_delete: "🗑️",
97
+ skills_run: "▶️",
91
98
  };
92
99
  const icon = icons[toolName] || "🔧";
93
100
  log(icon, `${colors.bright}${toolName}${colors.reset}${details ? ` ${colors.dim}${details}${colors.reset}` : ""}`, colors.cyan);
@@ -1009,32 +1016,32 @@ const TOOLS = [
1009
1016
  },
1010
1017
  {
1011
1018
  name: "tasks_start",
1012
- description: 'Mark a task as "in_progress". Use this when you start working on a task. The response includes a dashboard URL — always show it to the user.',
1019
+ description: 'Mark a task as "in_progress". Use this when you start working on a task. Accepts any identifier: UUID, short_id, or task title. The response includes a dashboard URL — always show it to the user.',
1013
1020
  annotations: { title: "Start Task", destructiveHint: true },
1014
1021
  inputSchema: {
1015
1022
  type: "object",
1016
1023
  properties: {
1017
- issue_id: {
1024
+ identifier: {
1018
1025
  type: "string",
1019
- description: "Task UUID to start working on",
1026
+ description: "Task identifier — can be a UUID, short_id (e.g. 'hpiu09'), or task title/name",
1020
1027
  },
1021
1028
  },
1022
- required: ["issue_id"],
1029
+ required: ["identifier"],
1023
1030
  },
1024
1031
  },
1025
1032
  {
1026
1033
  name: "tasks_resolve",
1027
- description: 'Mark a task as "resolved". Use this when you finish working on a task. The response includes a dashboard URL — always show it to the user.',
1034
+ description: 'Mark a task as "resolved". Use this when you finish working on a task. Accepts any identifier: UUID, short_id, or task title. The response includes a dashboard URL — always show it to the user.',
1028
1035
  annotations: { title: "Resolve Task", destructiveHint: true },
1029
1036
  inputSchema: {
1030
1037
  type: "object",
1031
1038
  properties: {
1032
- issue_id: {
1039
+ identifier: {
1033
1040
  type: "string",
1034
- description: "Task UUID to mark as resolved",
1041
+ description: "Task identifier — can be a UUID, short_id (e.g. 'hpiu09'), or task title/name",
1035
1042
  },
1036
1043
  },
1037
- required: ["issue_id"],
1044
+ required: ["identifier"],
1038
1045
  },
1039
1046
  },
1040
1047
  {
@@ -1099,18 +1106,14 @@ const TOOLS = [
1099
1106
  },
1100
1107
  {
1101
1108
  name: "tasks_update",
1102
- description: "Update a task's title, description, status, priority, tags, due date, or assignee. Provide issue_id or short_id to identify the task, plus one or more fields to update. The response includes a dashboard URL — always show it to the user.",
1109
+ description: "Update a task's title, description, status, priority, tags, due date, or assignee. Accepts any identifier: UUID, short_id, or task title. The response includes a dashboard URL — always show it to the user.",
1103
1110
  annotations: { title: "Update Task", destructiveHint: true },
1104
1111
  inputSchema: {
1105
1112
  type: "object",
1106
1113
  properties: {
1107
- issue_id: {
1114
+ identifier: {
1108
1115
  type: "string",
1109
- description: "Task UUID (optional if short_id is provided)",
1110
- },
1111
- short_id: {
1112
- type: "string",
1113
- description: 'Short ID of the task (e.g., "4w3123") - alternative to issue_id',
1116
+ description: "Task identifier — can be a UUID, short_id (e.g. 'hpiu09'), or task title/name",
1114
1117
  },
1115
1118
  title: {
1116
1119
  type: "string",
@@ -1149,41 +1152,33 @@ const TOOLS = [
1149
1152
  },
1150
1153
  {
1151
1154
  name: "tasks_assign",
1152
- description: "Assign a task to a collaborator by their email address. The response includes a dashboard URL — always show it to the user.",
1155
+ description: "Assign a task to a collaborator by their email address. Accepts any task identifier: UUID, short_id, or task title. The response includes a dashboard URL — always show it to the user.",
1153
1156
  annotations: { title: "Assign Task", destructiveHint: true },
1154
1157
  inputSchema: {
1155
1158
  type: "object",
1156
1159
  properties: {
1157
- issue_id: {
1160
+ identifier: {
1158
1161
  type: "string",
1159
- description: "Task UUID to assign (optional if short_id or issue_title is provided)",
1160
- },
1161
- short_id: {
1162
- type: "string",
1163
- description: 'Short ID of the task (e.g., "4w3123") - easier to reference than full UUID',
1164
- },
1165
- issue_title: {
1166
- type: "string",
1167
- description: "Task title to search for (optional if issue_id or short_id is provided)",
1162
+ description: "Task identifier can be a UUID, short_id (e.g. 'hpiu09'), or task title/name",
1168
1163
  },
1169
1164
  assignee_email: {
1170
1165
  type: "string",
1171
1166
  description: "Email of the collaborator to assign the task to",
1172
1167
  },
1173
1168
  },
1174
- required: ["assignee_email"],
1169
+ required: ["identifier", "assignee_email"],
1175
1170
  },
1176
1171
  },
1177
1172
  {
1178
1173
  name: "tasks_resolve_by_name",
1179
- description: "Resolve a task by searching for it by title. Use this when you only know the task name. The response includes a dashboard URL — always show it to the user.",
1174
+ description: "Resolve a task by searching for it by title, short_id, or UUID. Use this when you have any identifier for the task. The response includes a dashboard URL — always show it to the user.",
1180
1175
  annotations: { title: "Resolve Task by Name", destructiveHint: true },
1181
1176
  inputSchema: {
1182
1177
  type: "object",
1183
1178
  properties: {
1184
1179
  title: {
1185
1180
  type: "string",
1186
- description: "Task title to search for (partial match)",
1181
+ description: "Task identifier can be a title (partial match), short_id, or UUID",
1187
1182
  },
1188
1183
  },
1189
1184
  required: ["title"],
@@ -1191,52 +1186,52 @@ const TOOLS = [
1191
1186
  },
1192
1187
  {
1193
1188
  name: "tasks_delete",
1194
- description: "Permanently delete a task by ID or short_id. Also deletes related comments, activity, and notifications.",
1189
+ description: "Permanently delete a task. Accepts any identifier: UUID, short_id, or task title. Also deletes related comments, activity, and notifications.",
1195
1190
  annotations: { title: "Delete Task", destructiveHint: true },
1196
1191
  inputSchema: {
1197
1192
  type: "object",
1198
1193
  properties: {
1199
- issue_id: {
1194
+ identifier: {
1200
1195
  type: "string",
1201
- description: "Task UUID or short_id (6 alphanumeric characters)",
1196
+ description: "Task identifier — can be a UUID, short_id (e.g. 'hpiu09'), or task title/name",
1202
1197
  },
1203
1198
  },
1204
- required: ["issue_id"],
1199
+ required: ["identifier"],
1205
1200
  },
1206
1201
  },
1207
1202
  // ============ Task Comments ============
1208
1203
  {
1209
1204
  name: "tasks_list_comments",
1210
- description: "List comments on a task. Use the task UUID or short_id to identify the task.",
1205
+ description: "List comments on a task. Accepts any identifier: UUID, short_id, or task title.",
1211
1206
  annotations: { title: "List Task Comments", readOnlyHint: true },
1212
1207
  inputSchema: {
1213
1208
  type: "object",
1214
1209
  properties: {
1215
- issue_id: {
1210
+ identifier: {
1216
1211
  type: "string",
1217
- description: "Task UUID or short_id (6 alphanumeric characters)",
1212
+ description: "Task identifier — can be a UUID, short_id (e.g. 'hpiu09'), or task title/name",
1218
1213
  },
1219
1214
  },
1220
- required: ["issue_id"],
1215
+ required: ["identifier"],
1221
1216
  },
1222
1217
  },
1223
1218
  {
1224
1219
  name: "tasks_add_comment",
1225
- description: "Add a comment to a task. The response includes the task dashboard URL — always show it to the user.",
1220
+ description: "Add a comment to a task. Accepts any task identifier: UUID, short_id, or task title. The response includes the task dashboard URL — always show it to the user.",
1226
1221
  annotations: { title: "Add Task Comment", destructiveHint: true },
1227
1222
  inputSchema: {
1228
1223
  type: "object",
1229
1224
  properties: {
1230
- issue_id: {
1225
+ identifier: {
1231
1226
  type: "string",
1232
- description: "Task UUID or short_id (6 alphanumeric characters)",
1227
+ description: "Task identifier — can be a UUID, short_id (e.g. 'hpiu09'), or task title/name",
1233
1228
  },
1234
1229
  content: {
1235
1230
  type: "string",
1236
1231
  description: "Comment text to add",
1237
1232
  },
1238
1233
  },
1239
- required: ["issue_id", "content"],
1234
+ required: ["identifier", "content"],
1240
1235
  },
1241
1236
  },
1242
1237
  // ============ Collaborators ============
@@ -1287,6 +1282,109 @@ const TOOLS = [
1287
1282
  required: ["email"],
1288
1283
  },
1289
1284
  },
1285
+ {
1286
+ name: "skills_list",
1287
+ description: "List all Skills in a project. Returns skills with their name, description, model, and prompt body.",
1288
+ annotations: { title: "List Skills", readOnlyHint: true },
1289
+ inputSchema: {
1290
+ type: "object",
1291
+ properties: {
1292
+ project_id: { type: "string", description: "Project UUID" },
1293
+ },
1294
+ required: ["project_id"],
1295
+ },
1296
+ },
1297
+ {
1298
+ name: "skills_get",
1299
+ description: "Get a single Skill by ID with full body.",
1300
+ annotations: { title: "Get Skill", readOnlyHint: true },
1301
+ inputSchema: {
1302
+ type: "object",
1303
+ properties: {
1304
+ id: { type: "string", description: "Skill UUID" },
1305
+ },
1306
+ required: ["id"],
1307
+ },
1308
+ },
1309
+ {
1310
+ name: "skills_create",
1311
+ description: "Create a new Skill in a project. The 'body' is a markdown prompt template that may use {{variable}} placeholders for input_params at run time.",
1312
+ annotations: { title: "Create Skill", destructiveHint: false },
1313
+ inputSchema: {
1314
+ type: "object",
1315
+ properties: {
1316
+ project_id: { type: "string" },
1317
+ name: { type: "string" },
1318
+ description: { type: "string" },
1319
+ body: {
1320
+ type: "string",
1321
+ description: "Markdown prompt template with optional {{var}} placeholders",
1322
+ },
1323
+ input_schema: {
1324
+ type: "object",
1325
+ description: "Optional JSON Schema for input_params",
1326
+ },
1327
+ llm_provider: {
1328
+ type: "string",
1329
+ enum: ["anthropic", "openai"],
1330
+ description: "Default 'anthropic'",
1331
+ },
1332
+ model: { type: "string", description: "e.g., claude-sonnet-4-6" },
1333
+ save_to_space_id: {
1334
+ type: "string",
1335
+ description: "Optional: save outputs as knowledge_items in this space",
1336
+ },
1337
+ },
1338
+ required: ["project_id", "name", "body", "model"],
1339
+ },
1340
+ },
1341
+ {
1342
+ name: "skills_update",
1343
+ description: "Update an existing Skill.",
1344
+ annotations: { title: "Update Skill", destructiveHint: true },
1345
+ inputSchema: {
1346
+ type: "object",
1347
+ properties: {
1348
+ id: { type: "string" },
1349
+ name: { type: "string" },
1350
+ description: { type: "string" },
1351
+ body: { type: "string" },
1352
+ input_schema: { type: "object" },
1353
+ llm_provider: { type: "string", enum: ["anthropic", "openai"] },
1354
+ model: { type: "string" },
1355
+ save_to_space_id: { type: "string" },
1356
+ },
1357
+ required: ["id"],
1358
+ },
1359
+ },
1360
+ {
1361
+ name: "skills_delete",
1362
+ description: "Delete a Skill.",
1363
+ annotations: { title: "Delete Skill", destructiveHint: true },
1364
+ inputSchema: {
1365
+ type: "object",
1366
+ properties: {
1367
+ id: { type: "string" },
1368
+ },
1369
+ required: ["id"],
1370
+ },
1371
+ },
1372
+ {
1373
+ name: "skills_run",
1374
+ description: "Execute a Skill on the configured LLM, optionally storing the output as a knowledge_item, and returns the result. Available to all project members.",
1375
+ annotations: { title: "Run Skill", destructiveHint: false },
1376
+ inputSchema: {
1377
+ type: "object",
1378
+ properties: {
1379
+ skill_id: { type: "string" },
1380
+ input_params: {
1381
+ type: "object",
1382
+ description: "Variables substituted into the skill body",
1383
+ },
1384
+ },
1385
+ required: ["skill_id"],
1386
+ },
1387
+ },
1290
1388
  ];
1291
1389
  // ============ Main Server ============
1292
1390
  async function main() {
@@ -2340,12 +2438,24 @@ async function main() {
2340
2438
  };
2341
2439
  }
2342
2440
  case "tasks_start": {
2343
- const issueId = args?.issue_id || args?.task_id;
2344
- if (!issueId) {
2345
- throw new Error("issue_id is required");
2441
+ const ident = resolveTaskIdentifier(args);
2442
+ if (!ident) {
2443
+ throw new Error("identifier is required — pass a UUID, short_id, or task title");
2444
+ }
2445
+ logTool(name, ident.value);
2446
+ let result;
2447
+ if (ident.type === "title") {
2448
+ // Find by title first, then start
2449
+ const { issues } = await apiClient.listTasks({ status: "all", limit: 100 });
2450
+ const titleLower = ident.value.toLowerCase();
2451
+ const match = issues?.find((i) => i.title.toLowerCase().includes(titleLower) || i.title.toLowerCase() === titleLower);
2452
+ if (!match)
2453
+ throw new Error(`No task found matching "${ident.value}"`);
2454
+ result = await apiClient.updateTaskStatus(match.id, "in_progress");
2455
+ }
2456
+ else {
2457
+ result = await apiClient.updateTaskStatus(ident.value, "in_progress");
2346
2458
  }
2347
- logTool(name, issueId);
2348
- const result = await apiClient.updateTaskStatus(issueId, "in_progress");
2349
2459
  const elapsed = Date.now() - startTime;
2350
2460
  logSuccess(`Started working on "${result.issue?.title}" in ${elapsed}ms`);
2351
2461
  return {
@@ -2365,12 +2475,20 @@ async function main() {
2365
2475
  };
2366
2476
  }
2367
2477
  case "tasks_resolve": {
2368
- const issueId = args?.issue_id || args?.task_id;
2369
- if (!issueId) {
2370
- throw new Error("issue_id is required");
2478
+ const ident = resolveTaskIdentifier(args);
2479
+ if (!ident) {
2480
+ throw new Error("identifier is required — pass a UUID, short_id, or task title");
2481
+ }
2482
+ logTool(name, ident.value);
2483
+ let result;
2484
+ if (ident.type === "title") {
2485
+ // Resolve by title search
2486
+ result = await apiClient.resolveTaskByName(ident.value);
2487
+ }
2488
+ else {
2489
+ // UUID or short_id — updateTaskStatus already handles both
2490
+ result = await apiClient.updateTaskStatus(ident.value, "resolved");
2371
2491
  }
2372
- logTool(name, issueId);
2373
- const result = await apiClient.updateTaskStatus(issueId, "resolved");
2374
2492
  const elapsed = Date.now() - startTime;
2375
2493
  logSuccess(`Resolved "${result.issue?.title}" in ${elapsed}ms`);
2376
2494
  return {
@@ -2471,21 +2589,19 @@ async function main() {
2471
2589
  };
2472
2590
  }
2473
2591
  case "tasks_assign": {
2474
- const issueId = args?.issue_id || args?.task_id;
2475
- const shortId = args?.short_id;
2476
- const issueTitle = args?.issue_title || args?.title;
2592
+ const ident = resolveTaskIdentifier(args);
2477
2593
  const assigneeEmail = args?.assignee_email;
2478
2594
  if (!assigneeEmail) {
2479
2595
  throw new Error("assignee_email is required");
2480
2596
  }
2481
- if (!issueId && !shortId && !issueTitle) {
2482
- throw new Error("Either issue_id, short_id, or issue_title is required");
2597
+ if (!ident) {
2598
+ throw new Error("identifier is required — pass a UUID, short_id, or task title");
2483
2599
  }
2484
2600
  logTool(name, `→ ${assigneeEmail}`);
2485
2601
  const result = await apiClient.assignTask({
2486
- issue_id: issueId,
2487
- short_id: shortId,
2488
- issue_title: issueTitle,
2602
+ issue_id: ident.type === "uuid" ? ident.value : undefined,
2603
+ short_id: ident.type === "short_id" ? ident.value : undefined,
2604
+ issue_title: ident.type === "title" ? ident.value : undefined,
2489
2605
  assignee_email: assigneeEmail,
2490
2606
  });
2491
2607
  const elapsed = Date.now() - startTime;
@@ -2500,12 +2616,19 @@ async function main() {
2500
2616
  };
2501
2617
  }
2502
2618
  case "tasks_resolve_by_name": {
2503
- const title = args?.title || args?.name;
2504
- if (!title) {
2505
- throw new Error("title is required");
2619
+ const ident = resolveTaskIdentifier(args);
2620
+ if (!ident) {
2621
+ throw new Error("identifier is required — pass a title, short_id, or UUID");
2622
+ }
2623
+ logTool(name, `"${ident.value}"`);
2624
+ let result;
2625
+ if (ident.type === "title") {
2626
+ result = await apiClient.resolveTaskByName(ident.value);
2627
+ }
2628
+ else {
2629
+ // UUID or short_id — resolve directly
2630
+ result = await apiClient.updateTaskStatus(ident.value, "resolved");
2506
2631
  }
2507
- logTool(name, `"${title}"`);
2508
- const result = await apiClient.resolveTaskByName(title);
2509
2632
  const elapsed = Date.now() - startTime;
2510
2633
  logSuccess(`Resolved "${result.issue?.title}" in ${elapsed}ms`);
2511
2634
  return {
@@ -2521,12 +2644,21 @@ async function main() {
2521
2644
  };
2522
2645
  }
2523
2646
  case "tasks_delete": {
2524
- const issueId = args?.issue_id || args?.task_id;
2525
- if (!issueId) {
2526
- throw new Error("issue_id is required");
2647
+ const ident = resolveTaskIdentifier(args);
2648
+ if (!ident) {
2649
+ throw new Error("identifier is required — pass a UUID, short_id, or task title");
2650
+ }
2651
+ logTool(name, ident.value);
2652
+ let deleteId = ident.value;
2653
+ if (ident.type === "title") {
2654
+ const { issues } = await apiClient.listTasks({ status: "all", limit: 100 });
2655
+ const titleLower = ident.value.toLowerCase();
2656
+ const match = issues?.find((i) => i.title.toLowerCase().includes(titleLower) || i.title.toLowerCase() === titleLower);
2657
+ if (!match)
2658
+ throw new Error(`No task found matching "${ident.value}"`);
2659
+ deleteId = match.id;
2527
2660
  }
2528
- logTool(name, issueId);
2529
- const result = await apiClient.deleteTask(issueId);
2661
+ const result = await apiClient.deleteTask(deleteId);
2530
2662
  const elapsed = Date.now() - startTime;
2531
2663
  logSuccess(`Deleted task "${result.title}" in ${elapsed}ms`);
2532
2664
  return {
@@ -2542,10 +2674,20 @@ async function main() {
2542
2674
  };
2543
2675
  }
2544
2676
  case "tasks_update": {
2545
- const issueId = args?.issue_id || args?.task_id;
2546
- const shortId = args?.short_id;
2547
- if (!issueId && !shortId) {
2548
- throw new Error("Either issue_id or short_id is required");
2677
+ const ident = resolveTaskIdentifier(args);
2678
+ if (!ident) {
2679
+ throw new Error("identifier is required — pass a UUID, short_id, or task title");
2680
+ }
2681
+ // Resolve title to UUID/short_id for the API
2682
+ let issueId = ident.type === "uuid" ? ident.value : undefined;
2683
+ let shortId = ident.type === "short_id" ? ident.value : undefined;
2684
+ if (ident.type === "title") {
2685
+ const { issues } = await apiClient.listTasks({ status: "all", limit: 100 });
2686
+ const titleLower = ident.value.toLowerCase();
2687
+ const match = issues?.find((i) => i.title.toLowerCase().includes(titleLower) || i.title.toLowerCase() === titleLower);
2688
+ if (!match)
2689
+ throw new Error(`No task found matching "${ident.value}"`);
2690
+ issueId = match.id;
2549
2691
  }
2550
2692
  const updateFields = {};
2551
2693
  if (args?.title !== undefined)
@@ -2594,22 +2736,28 @@ async function main() {
2594
2736
  };
2595
2737
  }
2596
2738
  case "tasks_list_comments": {
2597
- const issueId = args?.issue_id || args?.task_id;
2598
- if (!issueId) {
2599
- throw new Error("issue_id is required");
2739
+ const ident = resolveTaskIdentifier(args);
2740
+ if (!ident) {
2741
+ throw new Error("identifier is required — pass a UUID, short_id, or task title");
2600
2742
  }
2601
- logTool(name, issueId);
2602
- // Resolve short_id to UUID if needed
2603
- let resolvedId = issueId;
2604
- const isShortId = /^[a-z0-9]{6}$/i.test(issueId) && !issueId.includes("-");
2605
- if (isShortId) {
2743
+ logTool(name, ident.value);
2744
+ // Resolve to UUID
2745
+ let resolvedId = ident.value;
2746
+ if (ident.type !== "uuid") {
2606
2747
  const { issues } = await apiClient.listTasks({
2607
2748
  status: "all",
2608
2749
  limit: 100,
2609
2750
  });
2610
- const match = issues?.find((i) => i.short_id?.toLowerCase() === issueId.toLowerCase());
2751
+ let match;
2752
+ if (ident.type === "short_id") {
2753
+ match = issues?.find((i) => i.short_id?.toLowerCase() === ident.value.toLowerCase());
2754
+ }
2755
+ else {
2756
+ const titleLower = ident.value.toLowerCase();
2757
+ match = issues?.find((i) => i.title.toLowerCase().includes(titleLower) || i.title.toLowerCase() === titleLower);
2758
+ }
2611
2759
  if (!match) {
2612
- throw new Error(`No task found with short_id "${issueId}"`);
2760
+ throw new Error(`No task found matching "${ident.value}"`);
2613
2761
  }
2614
2762
  resolvedId = match.id;
2615
2763
  }
@@ -2645,26 +2793,32 @@ async function main() {
2645
2793
  };
2646
2794
  }
2647
2795
  case "tasks_add_comment": {
2648
- const issueId = args?.issue_id || args?.task_id;
2796
+ const ident = resolveTaskIdentifier(args);
2649
2797
  const content = args?.content;
2650
- if (!issueId) {
2651
- throw new Error("issue_id is required");
2798
+ if (!ident) {
2799
+ throw new Error("identifier is required — pass a UUID, short_id, or task title");
2652
2800
  }
2653
2801
  if (!content) {
2654
2802
  throw new Error("content is required");
2655
2803
  }
2656
- logTool(name, issueId);
2657
- // Resolve short_id to UUID if needed
2658
- let resolvedId = issueId;
2659
- const isShortId = /^[a-z0-9]{6}$/i.test(issueId) && !issueId.includes("-");
2660
- if (isShortId) {
2804
+ logTool(name, ident.value);
2805
+ // Resolve to UUID
2806
+ let resolvedId = ident.value;
2807
+ if (ident.type !== "uuid") {
2661
2808
  const { issues } = await apiClient.listTasks({
2662
2809
  status: "all",
2663
2810
  limit: 100,
2664
2811
  });
2665
- const match = issues?.find((i) => i.short_id?.toLowerCase() === issueId.toLowerCase());
2812
+ let match;
2813
+ if (ident.type === "short_id") {
2814
+ match = issues?.find((i) => i.short_id?.toLowerCase() === ident.value.toLowerCase());
2815
+ }
2816
+ else {
2817
+ const titleLower = ident.value.toLowerCase();
2818
+ match = issues?.find((i) => i.title.toLowerCase().includes(titleLower) || i.title.toLowerCase() === titleLower);
2819
+ }
2666
2820
  if (!match) {
2667
- throw new Error(`No task found with short_id "${issueId}"`);
2821
+ throw new Error(`No task found matching "${ident.value}"`);
2668
2822
  }
2669
2823
  resolvedId = match.id;
2670
2824
  }
@@ -2968,6 +3122,88 @@ https://github.com/contextforge/contextforge-mcp
2968
3122
  ],
2969
3123
  };
2970
3124
  }
3125
+ case "skills_list": {
3126
+ const input = SkillsListInputSchema.parse(args);
3127
+ logTool(name, input.project_id);
3128
+ const skills = await apiClient.listSkills(input);
3129
+ return {
3130
+ content: [
3131
+ {
3132
+ type: "text",
3133
+ text: JSON.stringify(skills, null, 2),
3134
+ },
3135
+ ],
3136
+ };
3137
+ }
3138
+ case "skills_get": {
3139
+ const input = SkillsGetInputSchema.parse(args);
3140
+ logTool(name, input.id);
3141
+ const skill = await apiClient.getSkill(input);
3142
+ return {
3143
+ content: [
3144
+ {
3145
+ type: "text",
3146
+ text: JSON.stringify(skill, null, 2),
3147
+ },
3148
+ ],
3149
+ };
3150
+ }
3151
+ case "skills_create": {
3152
+ const input = SkillsCreateInputSchema.parse(args);
3153
+ logTool(name, `"${input.name}"`);
3154
+ const skill = await apiClient.createSkill(input);
3155
+ return {
3156
+ content: [
3157
+ {
3158
+ type: "text",
3159
+ text: `✅ Created skill ${skill.name} (id: ${skill.id})`,
3160
+ },
3161
+ ],
3162
+ };
3163
+ }
3164
+ case "skills_update": {
3165
+ const input = SkillsUpdateInputSchema.parse(args);
3166
+ logTool(name, input.id);
3167
+ const skill = await apiClient.updateSkill(input);
3168
+ return {
3169
+ content: [
3170
+ {
3171
+ type: "text",
3172
+ text: `✅ Updated skill ${skill.name}`,
3173
+ },
3174
+ ],
3175
+ };
3176
+ }
3177
+ case "skills_delete": {
3178
+ const input = SkillsDeleteInputSchema.parse(args);
3179
+ logTool(name, input.id);
3180
+ await apiClient.deleteSkill(input);
3181
+ return {
3182
+ content: [
3183
+ {
3184
+ type: "text",
3185
+ text: `🗑️ Skill ${input.id} deleted`,
3186
+ },
3187
+ ],
3188
+ };
3189
+ }
3190
+ case "skills_run": {
3191
+ const input = SkillsRunInputSchema.parse(args);
3192
+ logTool(name, input.skill_id);
3193
+ const result = await apiClient.runSkill(input);
3194
+ const cost = typeof result?.cost_usd === "number"
3195
+ ? result.cost_usd.toFixed(4)
3196
+ : "0";
3197
+ const summary = `**Output:**\n\n${result?.output ?? ""}\n\n---\n*Tokens: ${result?.tokens_input ?? 0}/${result?.tokens_output ?? 0} • Cost: $${cost}*`;
3198
+ return {
3199
+ content: [
3200
+ {
3201
+ type: "text",
3202
+ text: summary,
3203
+ },
3204
+ ],
3205
+ };
3206
+ }
2971
3207
  default:
2972
3208
  logError(`Unknown tool: ${name}`);
2973
3209
  return {