exponential-mcp 0.6.0 → 0.8.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 (53) hide show
  1. package/README.md +27 -0
  2. package/dist/domains/actions.d.ts +2 -0
  3. package/dist/domains/actions.js +157 -0
  4. package/dist/domains/areas.d.ts +2 -0
  5. package/dist/domains/areas.js +22 -0
  6. package/dist/domains/comments.d.ts +2 -0
  7. package/dist/domains/comments.js +165 -0
  8. package/dist/domains/contacts.d.ts +2 -0
  9. package/dist/domains/contacts.js +105 -0
  10. package/dist/domains/deals.d.ts +2 -0
  11. package/dist/domains/deals.js +77 -0
  12. package/dist/domains/decisions.d.ts +2 -0
  13. package/dist/domains/decisions.js +160 -0
  14. package/dist/domains/epics.d.ts +2 -0
  15. package/dist/domains/epics.js +54 -0
  16. package/dist/domains/features.d.ts +2 -0
  17. package/dist/domains/features.js +89 -0
  18. package/dist/domains/framework.d.ts +34 -0
  19. package/dist/domains/framework.js +94 -0
  20. package/dist/domains/goals.d.ts +2 -0
  21. package/dist/domains/goals.js +121 -0
  22. package/dist/domains/index.d.ts +5 -0
  23. package/dist/domains/index.js +48 -0
  24. package/dist/domains/keyResults.d.ts +2 -0
  25. package/dist/domains/keyResults.js +112 -0
  26. package/dist/domains/labels.d.ts +2 -0
  27. package/dist/domains/labels.js +77 -0
  28. package/dist/domains/meetings.d.ts +2 -0
  29. package/dist/domains/meetings.js +90 -0
  30. package/dist/domains/organizations.d.ts +2 -0
  31. package/dist/domains/organizations.js +38 -0
  32. package/dist/domains/pages.d.ts +2 -0
  33. package/dist/domains/pages.js +44 -0
  34. package/dist/domains/products.d.ts +2 -0
  35. package/dist/domains/products.js +57 -0
  36. package/dist/domains/projects.d.ts +2 -0
  37. package/dist/domains/projects.js +57 -0
  38. package/dist/domains/requirements.d.ts +2 -0
  39. package/dist/domains/requirements.js +39 -0
  40. package/dist/domains/scopes.d.ts +2 -0
  41. package/dist/domains/scopes.js +42 -0
  42. package/dist/domains/stories.d.ts +2 -0
  43. package/dist/domains/stories.js +46 -0
  44. package/dist/domains/tickets.d.ts +2 -0
  45. package/dist/domains/tickets.js +135 -0
  46. package/dist/domains/time.d.ts +2 -0
  47. package/dist/domains/time.js +64 -0
  48. package/dist/domains/workspaces.d.ts +2 -0
  49. package/dist/domains/workspaces.js +18 -0
  50. package/dist/index.js +40 -13
  51. package/dist/runTools.d.ts +29 -0
  52. package/dist/runTools.js +98 -0
  53. package/package.json +5 -4
@@ -0,0 +1,135 @@
1
+ import { z } from 'zod';
2
+ import { id, op } from './framework.js';
3
+ const TICKET_STATUSES = [
4
+ 'BACKLOG',
5
+ 'NEEDS_REFINEMENT',
6
+ 'READY_TO_PLAN',
7
+ 'COMMITTED',
8
+ 'IN_PROGRESS',
9
+ 'BLOCKED',
10
+ 'QA',
11
+ 'DONE',
12
+ 'DEPLOYED',
13
+ 'ARCHIVED',
14
+ ];
15
+ const TICKET_TYPES = ['BUG', 'FEATURE', 'CHORE', 'IMPROVEMENT', 'SPIKE', 'RESEARCH'];
16
+ const ticketId = z.string().describe('Ticket ID (the CUID, not the shortId or number — find it with `search`)');
17
+ const url = (what) => z.string().url().max(500).describe(`${what} URL`);
18
+ const clearable = (schema, what) => schema.nullable().optional().describe(`${what} (null to clear)`);
19
+ export const tickets = {
20
+ name: 'tickets',
21
+ description: 'Tickets: the unit of work in a product backlog, with blocking dependencies and linked actions. Always product-scoped (see `products`); they can point at a feature (`features`), scope (`scopes`), epic (`epics`) and cycle. Ticket comments are in the `comments` tool and labels in `labels`.',
22
+ operations: {
23
+ list: op({
24
+ summary: 'List every ticket in one product with full fields (body included), optionally filtered. No workspace-wide query — call once per product. prUrl/branchName are exact-match filters applied after the fetch.',
25
+ params: z.object({
26
+ productId: id('Product'),
27
+ status: z.enum(TICKET_STATUSES).optional(),
28
+ type: z.enum(TICKET_TYPES).optional(),
29
+ featureId: id('Feature').optional(),
30
+ epicId: id('Epic').optional(),
31
+ cycleId: id('Cycle').optional(),
32
+ assigneeId: id('User').optional(),
33
+ prUrl: z.string().optional().describe('Exact match on the ticket’s PR URL'),
34
+ branchName: z.string().optional().describe('Exact match on the ticket’s branch name'),
35
+ }),
36
+ run: (client, params) => client.tickets.list(params),
37
+ }),
38
+ get: op({
39
+ summary: 'Get one ticket in full: body, product, dependencies (dependsOn / requiredFor), linked actions and comments (newest first).',
40
+ params: z.object({ id: ticketId }),
41
+ run: (client, { id }) => client.tickets.get(id),
42
+ }),
43
+ create: op({
44
+ summary: 'Create a ticket in a product. Number and shortId are assigned server-side.',
45
+ params: z.object({
46
+ productId: id('Product'),
47
+ title: z.string().min(1).max(300),
48
+ body: z.string().optional().describe('Markdown body. Omit with templateId to start from the template’s body'),
49
+ type: z.enum(TICKET_TYPES).optional(),
50
+ status: z.enum(TICKET_STATUSES).optional(),
51
+ priority: z.number().int().min(0).max(4).optional().describe('0–4; lower is higher priority'),
52
+ points: z.number().optional(),
53
+ branchName: z.string().optional(),
54
+ prUrl: url('Pull request').optional(),
55
+ designUrl: url('Design').optional(),
56
+ specUrl: url('Spec').optional(),
57
+ links: z.record(z.string()).optional().describe('Extra named links, label → URL'),
58
+ epicId: id('Epic').optional(),
59
+ featureId: id('Feature').optional(),
60
+ cycleId: id('Cycle').optional(),
61
+ scopeId: id('Feature scope').optional(),
62
+ assigneeId: id('User').optional().describe('User ID; must be a member of the product’s workspace'),
63
+ templateId: id('Ticket template').optional(),
64
+ }),
65
+ run: (client, params) => client.tickets.create(params),
66
+ }),
67
+ update: op({
68
+ summary: 'Update a ticket. Only the fields you pass change.',
69
+ params: z.object({
70
+ id: ticketId,
71
+ title: z.string().min(1).max(300).optional(),
72
+ body: z.string().optional().describe('Markdown body; replaces the whole body'),
73
+ type: z.enum(TICKET_TYPES).optional(),
74
+ status: z.enum(TICKET_STATUSES).optional(),
75
+ priority: clearable(z.number().int().min(0).max(4), 'Priority 0–4, lower is higher'),
76
+ points: clearable(z.number(), 'Story points'),
77
+ branchName: clearable(z.string(), 'Branch name'),
78
+ prUrl: clearable(z.string().url().max(500), 'Pull request URL'),
79
+ designUrl: clearable(z.string().url().max(500), 'Design URL'),
80
+ specUrl: clearable(z.string().url().max(500), 'Spec URL'),
81
+ links: clearable(z.record(z.string()), 'Named links, label → URL; replaces the whole map'),
82
+ epicId: clearable(z.string(), 'Epic ID'),
83
+ featureId: clearable(z.string(), 'Feature ID'),
84
+ cycleId: clearable(z.string(), 'Cycle ID'),
85
+ scopeId: clearable(z.string(), 'Feature scope ID'),
86
+ assigneeId: clearable(z.string(), 'Assignee user ID'),
87
+ }),
88
+ run: (client, params) => client.tickets.update(params),
89
+ }),
90
+ delete: op({
91
+ summary: 'Delete a ticket permanently, with its comments and dependency edges. Linked actions are kept but unlinked.',
92
+ params: z.object({ id: ticketId }),
93
+ run: (client, { id }) => client.tickets.delete(id),
94
+ }),
95
+ search: op({
96
+ summary: 'Quick picker search within one product: matches title, shortId or ticket number and returns slim rows (id, number, shortId, title, status, priority, assignee), most recently updated first. Use `list` for full tickets or field filters.',
97
+ params: z.object({
98
+ productId: id('Product'),
99
+ query: z.string().max(200).optional().describe('Title substring, shortId, or a bare ticket number'),
100
+ excludeTicketId: id('Ticket to leave out of the results').optional(),
101
+ limit: z.number().int().min(1).max(50).optional().describe('Default 20'),
102
+ }),
103
+ run: (client, params) => client.tickets.search(params),
104
+ }),
105
+ add_dependency: op({
106
+ summary: 'Mark ticketId as blocked by dependsOnId. Both must be in the same product; rejects self-dependencies and cycles. Idempotent.',
107
+ params: z.object({
108
+ ticketId: id('Blocked ticket'),
109
+ dependsOnId: id('Blocking ticket'),
110
+ }),
111
+ run: (client, { ticketId, dependsOnId }) => client.tickets.addDependency(ticketId, dependsOnId),
112
+ }),
113
+ remove_dependency: op({
114
+ summary: 'Remove a "ticketId is blocked by dependsOnId" edge. Succeeds even if no such edge exists.',
115
+ params: z.object({
116
+ ticketId: id('Blocked ticket'),
117
+ dependsOnId: id('Blocking ticket'),
118
+ }),
119
+ run: (client, { ticketId, dependsOnId }) => client.tickets.removeDependency(ticketId, dependsOnId),
120
+ }),
121
+ link_action: op({
122
+ summary: 'Attach one of your own actions to a ticket (an action belongs to at most one ticket, so this moves it if already linked).',
123
+ params: z.object({
124
+ ticketId: id('Ticket'),
125
+ actionId: id('Action'),
126
+ }),
127
+ run: (client, { ticketId, actionId }) => client.tickets.linkAction(ticketId, actionId),
128
+ }),
129
+ unlink_action: op({
130
+ summary: 'Detach one of your own actions from whatever ticket it is linked to.',
131
+ params: z.object({ actionId: id('Action') }),
132
+ run: (client, { actionId }) => client.tickets.unlinkAction(actionId),
133
+ }),
134
+ },
135
+ };
@@ -0,0 +1,2 @@
1
+ import { type Domain } from './framework.js';
2
+ export declare const time: Domain;
@@ -0,0 +1,64 @@
1
+ import { z } from 'zod';
2
+ import { date, id, op } from './framework.js';
3
+ const TIME_ENTRY_STATUSES = ['PROPOSED', 'CONFIRMED'];
4
+ const WRITABLE_SOURCES = ['manual', 'claude-desktop', 'agent-run'];
5
+ const day = z
6
+ .string()
7
+ .describe("Calendar day as YYYY-MM-DD (or an ISO timestamp); resolved to local midnight in the MCP server's timezone");
8
+ const optionalWorkspaceId = z
9
+ .string()
10
+ .optional()
11
+ .describe('Workspace ID (see the `workspaces` tool); omit for all workspaces');
12
+ const entryFields = {
13
+ actionId: id('Action'),
14
+ startedAt: date('Start'),
15
+ endedAt: date('End (must be after startedAt)'),
16
+ source: z
17
+ .enum(WRITABLE_SOURCES)
18
+ .optional()
19
+ .describe('Defaults to manual. claude-desktop = Daily worklog proposed time; agent-run = unattended agent time'),
20
+ status: z
21
+ .enum(TIME_ENTRY_STATUSES)
22
+ .optional()
23
+ .describe('Defaults to CONFIRMED for a human; ignored (always PROPOSED) under an agent key'),
24
+ sourceRef: z
25
+ .string()
26
+ .optional()
27
+ .describe('Idempotency key, e.g. `claude-session:<sessionId>#<segmentIndex>`; makes the write an upsert'),
28
+ note: z.string().max(1000).optional().describe('One line shown in the day view'),
29
+ };
30
+ const partialEntry = z.object(entryFields).partial();
31
+ export const time = {
32
+ name: 'time',
33
+ description: 'Time entries logged against actions (the Daily worklog write path). An entry always belongs to the person whose time it was: under an agent key it is owned by the agent’s owner and forced to PROPOSED until they confirm the day. Actions live in the `actions` tool.',
34
+ operations: {
35
+ log: op({
36
+ summary: 'Log one completed entry with explicit bounds; never touches the running Timer. With sourceRef it upserts per owner (updates a PROPOSED entry, leaves a CONFIRMED one); without, every call creates a row. Outcome: created / updated / left / merged / dropped.',
37
+ params: z.object(entryFields),
38
+ run: (client, params) => client.time.log(params),
39
+ }),
40
+ log_batch: op({
41
+ summary: 'Log many entries in order, one call each; a failure is reported per entry ({ index, success, error }) instead of failing the batch. `defaults` fill fields an entry omits.',
42
+ params: z.object({
43
+ entries: z.array(partialEntry).describe('Entries; each needs actionId, startedAt, endedAt after defaults apply'),
44
+ defaults: partialEntry.optional(),
45
+ }),
46
+ run: (client, { entries, defaults }) => client.time.logBatch(entries, defaults),
47
+ }),
48
+ confirm_day: op({
49
+ summary: 'Flip every PROPOSED entry of yours starting on that day to CONFIRMED, moving the actions’ spent time. Human only — agent keys are FORBIDDEN. Returns { confirmed }.',
50
+ params: z.object({ date: day, workspaceId: optionalWorkspaceId }),
51
+ run: (client, { date, workspaceId }) => client.time.confirmDay(date, workspaceId),
52
+ }),
53
+ day_report: op({
54
+ summary: 'One day’s time as the /time Day tab shows it: entries joined to action/ticket/project/product, attention vs session vs agent-run minutes, per-product and per-action roll-ups. Under an agent key it is the owner’s day.',
55
+ params: z.object({ date: day, workspaceId: optionalWorkspaceId }),
56
+ run: (client, { date, workspaceId }) => client.time.dayReport(date, workspaceId),
57
+ }),
58
+ list: op({
59
+ summary: 'Your raw time entries touching one day, oldest first, each with its status.',
60
+ params: z.object({ date: day, workspaceId: optionalWorkspaceId }),
61
+ run: (client, { date, workspaceId }) => client.time.list(date, workspaceId),
62
+ }),
63
+ },
64
+ };
@@ -0,0 +1,2 @@
1
+ import { type Domain } from './framework.js';
2
+ export declare const workspaces: Domain;
@@ -0,0 +1,18 @@
1
+ import { z } from 'zod';
2
+ import { op, workspaceId } from './framework.js';
3
+ export const workspaces = {
4
+ name: 'workspaces',
5
+ description: 'Workspaces the user belongs to, and their members. Most other tools take a workspaceId from here; use list_members to resolve people for mentions and assignment.',
6
+ operations: {
7
+ list: op({
8
+ summary: 'List the workspaces you belong to.',
9
+ params: z.object({}),
10
+ run: (client) => client.workspaces.list(),
11
+ }),
12
+ list_members: op({
13
+ summary: 'Everyone in a workspace — direct members plus those with access via a linked team. Each row carries `mentionSyntax` to paste into a comment body.',
14
+ params: z.object({ workspaceId }),
15
+ run: (client, { workspaceId }) => client.workspaces.listMembers(workspaceId),
16
+ }),
17
+ },
18
+ };
package/dist/index.js CHANGED
@@ -11,6 +11,8 @@ import { readFileSync, existsSync } from 'fs';
11
11
  import { homedir } from 'os';
12
12
  import { dirname, join } from 'path';
13
13
  import { fileURLToPath } from 'url';
14
+ import { DOMAINS, runDomainTool, toolDefinition } from './domains/index.js';
15
+ import { RUN_TOOLS, RUN_TOOL_NAMES, runContextFromEnv, runRunTool } from './runTools.js';
14
16
  const LEGACY_CONFIG_PATH = join(homedir(), '.config', 'exponential-mcp', 'config.json');
15
17
  // Report the real package version rather than a hand-maintained literal that drifts.
16
18
  const PKG_VERSION = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf-8')).version;
@@ -72,23 +74,29 @@ function migrateLegacyConfig() {
72
74
  }
73
75
  }
74
76
  function loadClientConfig() {
77
+ // An explicit key in the environment wins over the stored config: a process
78
+ // that starts this server for a specific principal — `exponential runner
79
+ // start` launching it for an Assistant run with the Assistant's agent key —
80
+ // must act as that principal, not as whoever last ran `init` here.
81
+ const envToken = process.env.EXPONENTIAL_API_KEY || process.env.EXPONENTIAL_API_TOKEN;
82
+ if (envToken) {
83
+ assertTokenIsUsable(envToken, 'EXPONENTIAL_API_KEY');
84
+ return {
85
+ token: envToken,
86
+ apiUrl: process.env.EXPONENTIAL_API_URL ||
87
+ process.env.EXPONENTIAL_BASE_URL ||
88
+ 'https://www.exponential.im',
89
+ };
90
+ }
75
91
  migrateLegacyConfig();
76
92
  if (configStore.isAuthenticated()) {
77
93
  const config = configStore.loadConfig();
78
94
  assertTokenIsUsable(config.token, 'stored config');
79
95
  return { token: config.token, apiUrl: config.apiUrl };
80
96
  }
81
- const token = process.env.EXPONENTIAL_API_KEY || process.env.EXPONENTIAL_API_TOKEN;
82
- const apiUrl = process.env.EXPONENTIAL_API_URL ||
83
- process.env.EXPONENTIAL_BASE_URL ||
84
- 'https://www.exponential.im';
85
- if (!token) {
86
- console.error('Error: No API key found.');
87
- console.error('Run "npx exponential-mcp init" to set up, or set EXPONENTIAL_API_KEY env var.');
88
- process.exit(1);
89
- }
90
- assertTokenIsUsable(token, 'EXPONENTIAL_API_KEY');
91
- return { token, apiUrl };
97
+ console.error('Error: No API key found.');
98
+ console.error('Run "npx exponential-mcp init" to set up, or set EXPONENTIAL_API_KEY env var.');
99
+ process.exit(1);
92
100
  }
93
101
  /**
94
102
  * `undefined` = leave the field alone, `null` = clear it, otherwise a Date.
@@ -568,13 +576,32 @@ async function main() {
568
576
  },
569
577
  });
570
578
  // List available tools
579
+ // Started for an Agent run (`exponential runner start` sets EXPONENTIAL_RUN_ID):
580
+ // the session gets the three run tools on top of everything else.
581
+ const runContext = runContextFromEnv();
571
582
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
572
- tools: TOOLS,
583
+ tools: [...(runContext ? RUN_TOOLS : []), ...TOOLS, ...[...DOMAINS.values()].map(toolDefinition)],
573
584
  }));
574
585
  // Handle tool calls
575
586
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
576
587
  const { name, arguments: args } = request.params;
577
588
  try {
589
+ if (runContext && RUN_TOOL_NAMES.has(name)) {
590
+ const result = await runRunTool(client, runContext, name, args);
591
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
592
+ }
593
+ const domain = DOMAINS.get(name);
594
+ if (domain) {
595
+ const result = await runDomainTool(domain, client, args);
596
+ return {
597
+ content: [
598
+ {
599
+ type: 'text',
600
+ text: JSON.stringify(result, null, 2),
601
+ },
602
+ ],
603
+ };
604
+ }
578
605
  switch (name) {
579
606
  case 'get_projects': {
580
607
  const projects = await client.projects.list({
@@ -877,7 +904,7 @@ async function main() {
877
904
  };
878
905
  }
879
906
  case 'search': {
880
- const results = await trpcClient.search.global.query({
907
+ const results = await client.search.global({
881
908
  query: args?.query,
882
909
  workspaceId: args?.workspaceId,
883
910
  limit: args?.limit,
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Run tools (Exponential ADR-0067, Agent PRD V2): how a session the local
3
+ * runner spawned talks to its Agent run. They exist only when the server was
4
+ * started for a run — `EXPONENTIAL_RUN_ID` set in the environment by
5
+ * `exponential runner start` (which also passes the Assistant's agent key as
6
+ * `EXPONENTIAL_API_KEY` and its name as `EXPONENTIAL_RUNNER_ID`). Outside a
7
+ * run they are not listed, so an ordinary MCP session never sees them.
8
+ *
9
+ * - `report_progress` — a one-line note for the run's transcript. The runner
10
+ * records it from the session's own tool stream, so this tool only
11
+ * acknowledges; nothing is written from here.
12
+ * - `ask_owner` — asks the owner and PAUSES the run: the app posts the question
13
+ * as a comment mentioning the owner and parks the run; the owner's reply
14
+ * starts a new run. The session must stop after calling it.
15
+ * - `finish_run` — closes the run with a public summary and whether the action
16
+ * is ready for the owner to confirm. The run never completes the action.
17
+ */
18
+ import type { Tool } from '@modelcontextprotocol/sdk/types.js';
19
+ import type { ExponentialClient } from 'exponential-sdk';
20
+ export interface RunContext {
21
+ runId: string;
22
+ runnerId?: string;
23
+ }
24
+ /** The run this server was started for, if any. */
25
+ export declare function runContextFromEnv(env?: NodeJS.ProcessEnv): RunContext | null;
26
+ export declare const RUN_TOOLS: Tool[];
27
+ export declare const RUN_TOOL_NAMES: Set<string>;
28
+ /** Dispatch one run tool. The caller has already checked `RUN_TOOL_NAMES.has(name)`. */
29
+ export declare function runRunTool(client: Pick<ExponentialClient, 'agentRuns'>, context: RunContext, name: string, args: Record<string, unknown> | undefined): Promise<unknown>;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Run tools (Exponential ADR-0067, Agent PRD V2): how a session the local
3
+ * runner spawned talks to its Agent run. They exist only when the server was
4
+ * started for a run — `EXPONENTIAL_RUN_ID` set in the environment by
5
+ * `exponential runner start` (which also passes the Assistant's agent key as
6
+ * `EXPONENTIAL_API_KEY` and its name as `EXPONENTIAL_RUNNER_ID`). Outside a
7
+ * run they are not listed, so an ordinary MCP session never sees them.
8
+ *
9
+ * - `report_progress` — a one-line note for the run's transcript. The runner
10
+ * records it from the session's own tool stream, so this tool only
11
+ * acknowledges; nothing is written from here.
12
+ * - `ask_owner` — asks the owner and PAUSES the run: the app posts the question
13
+ * as a comment mentioning the owner and parks the run; the owner's reply
14
+ * starts a new run. The session must stop after calling it.
15
+ * - `finish_run` — closes the run with a public summary and whether the action
16
+ * is ready for the owner to confirm. The run never completes the action.
17
+ */
18
+ import { z } from 'zod';
19
+ import { zodToJsonSchema } from 'zod-to-json-schema';
20
+ /** The run this server was started for, if any. */
21
+ export function runContextFromEnv(env = process.env) {
22
+ const runId = env.EXPONENTIAL_RUN_ID?.trim();
23
+ if (!runId)
24
+ return null;
25
+ return { runId, runnerId: env.EXPONENTIAL_RUNNER_ID?.trim() || undefined };
26
+ }
27
+ const reportProgressParams = z.object({
28
+ text: z.string().min(1).max(500).describe('One line, present tense: what you are doing now'),
29
+ });
30
+ const askOwnerParams = z.object({
31
+ question: z
32
+ .string()
33
+ .min(1)
34
+ .max(10000)
35
+ .describe('The question, with enough context that the owner can answer from their inbox'),
36
+ });
37
+ const finishRunParams = z.object({
38
+ summary: z
39
+ .string()
40
+ .min(1)
41
+ .max(10000)
42
+ .describe('What you did, found or delegated. Public: the requester, the owner and their teammates read it'),
43
+ readyToClose: z
44
+ .boolean()
45
+ .describe('true when the work is done and the owner only needs to confirm; false when follow-up is needed'),
46
+ });
47
+ function schema(params) {
48
+ const { $schema: _ignored, ...rest } = zodToJsonSchema(params, {
49
+ target: 'jsonSchema7',
50
+ $refStrategy: 'none',
51
+ });
52
+ return rest;
53
+ }
54
+ export const RUN_TOOLS = [
55
+ {
56
+ name: 'report_progress',
57
+ description: 'Post a one-line progress note to this run\'s transcript (owner-visible only; nobody is notified). Use it when you move to a new phase of the work.',
58
+ inputSchema: schema(reportProgressParams),
59
+ },
60
+ {
61
+ name: 'ask_owner',
62
+ description: 'Ask your owner a question you cannot answer yourself and PAUSE this run. The question is posted on the action mentioning the owner; their reply resumes the work in a new run. This must be your LAST call: do not call finish_run after it and do not keep working — stop immediately.',
63
+ inputSchema: schema(askOwnerParams),
64
+ },
65
+ {
66
+ name: 'finish_run',
67
+ description: 'Finish this run. Call it exactly once, as your last action, with a public summary and whether the action is ready for the owner to close. Never mark the action complete yourself; the owner confirms from their inbox.',
68
+ inputSchema: schema(finishRunParams),
69
+ },
70
+ ];
71
+ export const RUN_TOOL_NAMES = new Set(RUN_TOOLS.map((t) => t.name));
72
+ /** Dispatch one run tool. The caller has already checked `RUN_TOOL_NAMES.has(name)`. */
73
+ export async function runRunTool(client, context, name, args) {
74
+ switch (name) {
75
+ case 'report_progress': {
76
+ const { text } = reportProgressParams.parse(args ?? {});
77
+ return { ok: true, text };
78
+ }
79
+ case 'ask_owner': {
80
+ const { question } = askOwnerParams.parse(args ?? {});
81
+ const result = await client.agentRuns.finish(context.runId, { status: 'WAITING_ON_OWNER', question }, context.runnerId);
82
+ return {
83
+ ...result,
84
+ stop: true,
85
+ message: result.finished
86
+ ? 'Your question was posted and the run is now waiting on your owner. Stop here: make no further tool calls and end your turn.'
87
+ : 'The run is no longer running (it was cancelled); nothing was posted. Stop here.',
88
+ };
89
+ }
90
+ case 'finish_run': {
91
+ const { summary, readyToClose } = finishRunParams.parse(args ?? {});
92
+ const result = await client.agentRuns.finish(context.runId, { status: 'SUCCEEDED', summary, readyToClose }, context.runnerId);
93
+ return { ...result, message: result.finished ? 'Run finished. End your turn.' : 'The run had already ended; nothing changed. End your turn.' };
94
+ }
95
+ default:
96
+ throw new Error(`Unknown run tool "${name}"`);
97
+ }
98
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "exponential-mcp",
3
- "version": "0.6.0",
4
- "description": "MCP server for Exponential - connect Claude to your projects, actions, and goals",
3
+ "version": "0.8.0",
4
+ "description": "MCP server for Exponential - connect Claude to your projects, actions, goals, CRM, tickets and more",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "exponential-mcp": "./dist/cli.js"
@@ -26,8 +26,9 @@
26
26
  "dependencies": {
27
27
  "@modelcontextprotocol/sdk": "^1.30.0",
28
28
  "commander": "^12.0.0",
29
- "exponential-sdk": "^1.16.0",
30
- "zod": "^3.22.0"
29
+ "exponential-sdk": "^1.21.0",
30
+ "zod": "^3.22.0",
31
+ "zod-to-json-schema": "^3.25.2"
31
32
  },
32
33
  "devDependencies": {
33
34
  "@types/node": "^20.0.0",