exponential-mcp 0.7.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.
package/dist/cli.js CHANGED
File without changes
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ import { homedir } from 'os';
12
12
  import { dirname, join } from 'path';
13
13
  import { fileURLToPath } from 'url';
14
14
  import { DOMAINS, runDomainTool, toolDefinition } from './domains/index.js';
15
+ import { RUN_TOOLS, RUN_TOOL_NAMES, runContextFromEnv, runRunTool } from './runTools.js';
15
16
  const LEGACY_CONFIG_PATH = join(homedir(), '.config', 'exponential-mcp', 'config.json');
16
17
  // Report the real package version rather than a hand-maintained literal that drifts.
17
18
  const PKG_VERSION = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf-8')).version;
@@ -73,23 +74,29 @@ function migrateLegacyConfig() {
73
74
  }
74
75
  }
75
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
+ }
76
91
  migrateLegacyConfig();
77
92
  if (configStore.isAuthenticated()) {
78
93
  const config = configStore.loadConfig();
79
94
  assertTokenIsUsable(config.token, 'stored config');
80
95
  return { token: config.token, apiUrl: config.apiUrl };
81
96
  }
82
- const token = process.env.EXPONENTIAL_API_KEY || process.env.EXPONENTIAL_API_TOKEN;
83
- const apiUrl = process.env.EXPONENTIAL_API_URL ||
84
- process.env.EXPONENTIAL_BASE_URL ||
85
- 'https://www.exponential.im';
86
- if (!token) {
87
- console.error('Error: No API key found.');
88
- console.error('Run "npx exponential-mcp init" to set up, or set EXPONENTIAL_API_KEY env var.');
89
- process.exit(1);
90
- }
91
- assertTokenIsUsable(token, 'EXPONENTIAL_API_KEY');
92
- 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);
93
100
  }
94
101
  /**
95
102
  * `undefined` = leave the field alone, `null` = clear it, otherwise a Date.
@@ -569,13 +576,20 @@ async function main() {
569
576
  },
570
577
  });
571
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();
572
582
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
573
- tools: [...TOOLS, ...[...DOMAINS.values()].map(toolDefinition)],
583
+ tools: [...(runContext ? RUN_TOOLS : []), ...TOOLS, ...[...DOMAINS.values()].map(toolDefinition)],
574
584
  }));
575
585
  // Handle tool calls
576
586
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
577
587
  const { name, arguments: args } = request.params;
578
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
+ }
579
593
  const domain = DOMAINS.get(name);
580
594
  if (domain) {
581
595
  const result = await runDomainTool(domain, client, args);
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "exponential-mcp",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
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": {
@@ -26,7 +26,7 @@
26
26
  "dependencies": {
27
27
  "@modelcontextprotocol/sdk": "^1.30.0",
28
28
  "commander": "^12.0.0",
29
- "exponential-sdk": "^1.20.1",
29
+ "exponential-sdk": "^1.21.0",
30
30
  "zod": "^3.22.0",
31
31
  "zod-to-json-schema": "^3.25.2"
32
32
  },