exponential-mcp 0.6.0 → 0.7.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.
- package/README.md +27 -0
- package/dist/cli.js +0 -0
- package/dist/domains/actions.d.ts +2 -0
- package/dist/domains/actions.js +157 -0
- package/dist/domains/areas.d.ts +2 -0
- package/dist/domains/areas.js +22 -0
- package/dist/domains/comments.d.ts +2 -0
- package/dist/domains/comments.js +165 -0
- package/dist/domains/contacts.d.ts +2 -0
- package/dist/domains/contacts.js +105 -0
- package/dist/domains/deals.d.ts +2 -0
- package/dist/domains/deals.js +77 -0
- package/dist/domains/decisions.d.ts +2 -0
- package/dist/domains/decisions.js +160 -0
- package/dist/domains/epics.d.ts +2 -0
- package/dist/domains/epics.js +54 -0
- package/dist/domains/features.d.ts +2 -0
- package/dist/domains/features.js +89 -0
- package/dist/domains/framework.d.ts +34 -0
- package/dist/domains/framework.js +94 -0
- package/dist/domains/goals.d.ts +2 -0
- package/dist/domains/goals.js +121 -0
- package/dist/domains/index.d.ts +5 -0
- package/dist/domains/index.js +48 -0
- package/dist/domains/keyResults.d.ts +2 -0
- package/dist/domains/keyResults.js +112 -0
- package/dist/domains/labels.d.ts +2 -0
- package/dist/domains/labels.js +77 -0
- package/dist/domains/meetings.d.ts +2 -0
- package/dist/domains/meetings.js +90 -0
- package/dist/domains/organizations.d.ts +2 -0
- package/dist/domains/organizations.js +38 -0
- package/dist/domains/pages.d.ts +2 -0
- package/dist/domains/pages.js +44 -0
- package/dist/domains/products.d.ts +2 -0
- package/dist/domains/products.js +57 -0
- package/dist/domains/projects.d.ts +2 -0
- package/dist/domains/projects.js +57 -0
- package/dist/domains/requirements.d.ts +2 -0
- package/dist/domains/requirements.js +39 -0
- package/dist/domains/scopes.d.ts +2 -0
- package/dist/domains/scopes.js +42 -0
- package/dist/domains/stories.d.ts +2 -0
- package/dist/domains/stories.js +46 -0
- package/dist/domains/tickets.d.ts +2 -0
- package/dist/domains/tickets.js +135 -0
- package/dist/domains/time.d.ts +2 -0
- package/dist/domains/time.js +64 -0
- package/dist/domains/workspaces.d.ts +2 -0
- package/dist/domains/workspaces.js +18 -0
- package/dist/index.js +15 -2
- 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,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,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,7 @@ 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';
|
|
14
15
|
const LEGACY_CONFIG_PATH = join(homedir(), '.config', 'exponential-mcp', 'config.json');
|
|
15
16
|
// Report the real package version rather than a hand-maintained literal that drifts.
|
|
16
17
|
const PKG_VERSION = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf-8')).version;
|
|
@@ -569,12 +570,24 @@ async function main() {
|
|
|
569
570
|
});
|
|
570
571
|
// List available tools
|
|
571
572
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
572
|
-
tools: TOOLS,
|
|
573
|
+
tools: [...TOOLS, ...[...DOMAINS.values()].map(toolDefinition)],
|
|
573
574
|
}));
|
|
574
575
|
// Handle tool calls
|
|
575
576
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
576
577
|
const { name, arguments: args } = request.params;
|
|
577
578
|
try {
|
|
579
|
+
const domain = DOMAINS.get(name);
|
|
580
|
+
if (domain) {
|
|
581
|
+
const result = await runDomainTool(domain, client, args);
|
|
582
|
+
return {
|
|
583
|
+
content: [
|
|
584
|
+
{
|
|
585
|
+
type: 'text',
|
|
586
|
+
text: JSON.stringify(result, null, 2),
|
|
587
|
+
},
|
|
588
|
+
],
|
|
589
|
+
};
|
|
590
|
+
}
|
|
578
591
|
switch (name) {
|
|
579
592
|
case 'get_projects': {
|
|
580
593
|
const projects = await client.projects.list({
|
|
@@ -877,7 +890,7 @@ async function main() {
|
|
|
877
890
|
};
|
|
878
891
|
}
|
|
879
892
|
case 'search': {
|
|
880
|
-
const results = await
|
|
893
|
+
const results = await client.search.global({
|
|
881
894
|
query: args?.query,
|
|
882
895
|
workspaceId: args?.workspaceId,
|
|
883
896
|
limit: args?.limit,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "exponential-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server for Exponential - connect Claude to your projects, actions, and
|
|
3
|
+
"version": "0.7.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.
|
|
30
|
-
"zod": "^3.22.0"
|
|
29
|
+
"exponential-sdk": "^1.20.1",
|
|
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",
|