surecore-studio-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/index.js +47 -27
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -5,6 +5,34 @@ import { z } from 'zod';
5
5
  import { loadConfig } from './config.js';
6
6
  import { createApiClient } from './apiClient.js';
7
7
  import { CREATE_TASK_TYPES, listMyTasks, getTaskTool, searchTasks, updateTaskStatus, createTaskTool, updateTask, listComments, addComment, getAttachment, addAttachment, listMembersTool, } from './tools.js';
8
+ // ---------------------------------------------------------------------------
9
+ // Workspace cache — statuses and tags are user-definable and change at any
10
+ // time. Re-fetch at most once per TTL so every tool call uses a fresh list
11
+ // without hammering the API on every keystroke.
12
+ // ---------------------------------------------------------------------------
13
+ const WS_TTL_MS = 60_000;
14
+ class WorkspaceCache {
15
+ api;
16
+ ws = null;
17
+ fetchedAt = 0;
18
+ constructor(api) {
19
+ this.api = api;
20
+ }
21
+ async get() {
22
+ if (this.ws && Date.now() - this.fetchedAt < WS_TTL_MS)
23
+ return this.ws;
24
+ const [statuses, tags, members] = await Promise.all([
25
+ this.api.listStatuses(),
26
+ this.api.listTags().catch(() => []),
27
+ this.api.listMembers().catch(() => []),
28
+ ]);
29
+ this.ws = { statuses, tags, members };
30
+ this.fetchedAt = Date.now();
31
+ return this.ws;
32
+ }
33
+ /** Call after a local mutation so the next read is always fresh. */
34
+ invalidate() { this.fetchedAt = 0; }
35
+ }
8
36
  async function main() {
9
37
  const cfg = loadConfig();
10
38
  const api = createApiClient(cfg);
@@ -17,28 +45,20 @@ async function main() {
17
45
  process.stderr.write('surecore-tasks: could not authenticate — check SURECORE_PAT (generate one in Settings › Developer).\n');
18
46
  process.exit(1);
19
47
  }
20
- // Read the workspace's OWN vocabulary. Statuses and tags are user-definable,
21
- // so shipping a fixed list here would make anything created later invisible.
22
- // Tags and members are optional conveniences — a failure there must not stop
23
- // the server from serving tasks.
24
- const statuses = await api.listStatuses();
25
- const [tags, members] = await Promise.all([
26
- api.listTags().catch(() => []),
27
- api.listMembers().catch(() => []),
28
- ]);
29
- const ws = { statuses, tags, members };
30
- const statusList = statuses.map((s) => s.value);
31
- const tagList = tags.map((t) => t.value);
32
- // z.enum needs a non-empty tuple; statuses always have at least todo/done,
33
- // but a workspace can legitimately have zero tags.
34
- const statusSchema = z.enum(statusList);
35
- const tagSchema = tagList.length
36
- ? z.enum(tagList)
37
- : z.string().describe('This workspace has no tags defined yet.');
38
- const server = new McpServer({ name: 'surecore-tasks', version: '0.1.6' });
39
- server.tool('list_my_tasks', 'List the tasks assigned to you, optionally filtered by status, project or tag.', { status: statusSchema.optional(), projectId: z.string().optional(), tag: tagSchema.optional() }, (args) => listMyTasks(api, me, args));
40
- server.tool('get_task', 'Get one task in full: its fields, description, assignee names, comment thread and the list of attached files. Use this for "read task N" — a second call is not needed for comments.', { task: z.string().describe('Task number or appId') }, (args) => getTaskTool(api, args, ws, me));
41
- server.tool('search_tasks', `Search tasks by status, project, tag and/or free text. Statuses in this workspace: ${statusList.join(', ')}.${tagList.length ? ` Tags: ${tagList.join(', ')}.` : ''}`, { status: statusSchema.optional(), projectId: z.string().optional(), text: z.string().optional(), tag: tagSchema.optional() }, (args) => searchTasks(api, args, ws));
48
+ const wsCache = new WorkspaceCache(api);
49
+ // -------------------------------------------------------------------------
50
+ // Status and tag params use z.string() rather than z.enum(). The enum was
51
+ // baked in at startup and silently rejected any value added after the
52
+ // process started. The server validates the value anyway (and returns a
53
+ // clear error listing current valid values), so the enum gate is redundant
54
+ // and harmful.
55
+ // -------------------------------------------------------------------------
56
+ const statusParam = z.string().describe('A status value (e.g. "todo", "in-progress"). The server validates against the workspace\'s current list and returns valid options on a mismatch.');
57
+ const tagParam = z.string().describe('A tag value defined in this workspace.');
58
+ const server = new McpServer({ name: 'surecore-tasks', version: '0.1.7' });
59
+ server.tool('list_my_tasks', 'List the tasks assigned to you, optionally filtered by status, project or tag.', { status: statusParam.optional(), projectId: z.string().optional(), tag: tagParam.optional() }, async (args) => listMyTasks(api, me, args));
60
+ server.tool('get_task', 'Get one task in full: its fields, description, assignee names, comment thread and the list of attached files. Use this for "read task N" — a second call is not needed for comments.', { task: z.string().describe('Task number or appId') }, async (args) => getTaskTool(api, args, await wsCache.get(), me));
61
+ server.tool('search_tasks', 'Search tasks by status, project, tag and/or free text. Statuses and tags are fetched fresh on each call so newly created values are always recognised.', { status: statusParam.optional(), projectId: z.string().optional(), text: z.string().optional(), tag: tagParam.optional() }, async (args) => searchTasks(api, args, await wsCache.get()));
42
62
  server.tool('create_task', 'Create a new task or issue under a project (NOT a ticket — tickets come from support intake). Confirm the title, project, and type with the user before calling. `project` accepts a project name or id.', {
43
63
  title: z.string().describe('The task title'),
44
64
  project: z.string().describe('Project name (e.g. "AAIB") or project id'),
@@ -51,14 +71,14 @@ async function main() {
51
71
  title: z.string().optional(),
52
72
  description: z.string().optional(),
53
73
  priority: z.enum(['low', 'medium', 'high']).optional(),
54
- status: statusSchema.optional(),
74
+ status: statusParam.optional(),
55
75
  dueDate: z.string().optional().describe('YYYY-MM-DD'),
56
- tags: z.array(tagSchema).optional().describe('Replaces the task\'s tags'),
76
+ tags: z.array(tagParam).optional().describe('Replaces the task\'s tags'),
57
77
  assignees: z.array(z.string()).optional().describe('Member names or ids; replaces the current assignees'),
58
- }, (args) => updateTask(api, args, ws));
59
- server.tool('update_task_status', 'Move a task to a new status. Confirm the task and the target status with the user before calling. For customer-linked tickets this notifies the customer, exactly like the web app.', { task: z.string().describe('Task number or appId'), status: statusSchema }, (args) => updateTaskStatus(api, args, ws));
78
+ }, async (args) => { const ws = await wsCache.get(); return updateTask(api, args, ws); });
79
+ server.tool('update_task_status', 'Move a task to a new status. Confirm the task and the target status with the user before calling. For customer-linked tickets this notifies the customer, exactly like the web app.', { task: z.string().describe('Task number or appId'), status: statusParam }, async (args) => { const ws = await wsCache.get(); return updateTaskStatus(api, args, ws); });
60
80
  server.tool('get_attachment', 'Pull one file attached to a task into the conversation. Images are shown inline; other types are reported by name and type. `file` accepts the file name or its id.', { task: z.string().describe('Task number or appId'), file: z.string().describe('Attachment file name or id') }, (args) => getAttachment(api, args));
61
- server.tool('list_members', 'List the workspace members with their ids. Use this to turn a member id seen on a task into a person, or to find the id for an assignee.', {}, () => listMembersTool(ws));
81
+ server.tool('list_members', 'List the workspace members with their ids. Use this to turn a member id seen on a task into a person, or to find the id for an assignee.', {}, async () => listMembersTool(await wsCache.get()));
62
82
  server.tool('add_attachment', 'Attach a file from this machine to a task. Give the full path. Used to attach a work report to a task when the work is finished.', { task: z.string().describe('Task number or appId'), path: z.string().describe('Full path to the file on this machine') }, (args) => addAttachment(api, args));
63
83
  server.tool('list_comments', 'Read the comment thread on a task, oldest first.', { task: z.string().describe('Task number or appId') }, (args) => listComments(api, args));
64
84
  server.tool('add_comment', 'Post a comment on a task. It is always attributed to you (the token holder). Confirm the wording with the user before calling.', { task: z.string().describe('Task number or appId'), text: z.string().describe('The comment body') }, (args) => addComment(api, me, args));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "surecore-studio-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "MCP server for SurecoreStudio: read and update tasks, comments and attachments from Claude.",
5
5
  "license": "MIT",
6
6
  "author": "Surecore",
@@ -46,4 +46,4 @@
46
46
  "publishConfig": {
47
47
  "access": "public"
48
48
  }
49
- }
49
+ }