@withone/mcp 2.0.0 → 2.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.
package/README.md CHANGED
@@ -66,7 +66,7 @@ The server exposes three MCP tools:
66
66
 
67
67
  ### Finding actions
68
68
 
69
- `find_one_actions` takes one `requests` entry per operation, each a kebab-case `platform` and a short `intent` naming the operation alone (`"send a message to a channel"`, not the message). An optional `task` describes the whole job in general terms, which helps choose between similar actions. For each intent, One's decision model picks the action to use; the answer documents it, sets apart a substitute when the model was unsure, and lists a few alternatives. Large documents come back as a digest; call `find_one_actions` again with `load: [{ action_id, section }]` for a section it left out, `full: true` for the whole document, or `toc: true` for its table of contents. `ONE_ACTION_IDS`, `ONE_PERMISSIONS` and `ONE_CONNECTION_KEYS` apply to every answer: an action they refuse is never offered, and when they refuse the model's pick, the next allowed candidate takes its place.
69
+ `find_one_actions` takes one `requests` entry per operation, each a kebab-case `platform` and a short `intent` naming the operation alone (`"send a message to a channel"`, not the message). A `task` describes the whole job in general terms, and an `ai_model` names the model asking; agents are asked to send both with every find, since the task helps tell similar actions apart. For each intent, One's decision model picks the action to use; the answer documents it, sets apart a substitute when the model was unsure, and lists a few alternatives. Large documents come back as a digest; call `find_one_actions` again with `load: [{ action_id, section }]` for a section it left out, `full: true` for the whole document, or `toc: true` for its table of contents. `ONE_ACTION_IDS`, `ONE_PERMISSIONS` and `ONE_CONNECTION_KEYS` apply to every answer: an action they refuse is never offered, and when they refuse the model's pick, the next allowed candidate takes its place.
70
70
 
71
71
  Upgrading from 1.x: `search_one_platform_actions` and `get_one_action_knowledge` are removed. `find_one_actions` replaces both.
72
72
 
@@ -0,0 +1,28 @@
1
+ /**
2
+ * How a find names who asked, for One's record of what agents ask
3
+ * `find_one_actions` (pica-v2 #993).
4
+ *
5
+ * `x-one-source: cli` says the call came through One's own local tooling
6
+ * rather than a script holding the same key; this server reaches One through
7
+ * the same door as the CLI, and `cli` is the only surface core lets a client
8
+ * claim. `x-one-agent` names the MCP client that connected, as its
9
+ * `clientInfo` gave it (`claude-code/2.4.1`), and `x-one-agent-model` the model
10
+ * the agent says it is running as, which only the agent knows. All three are
11
+ * declared, never authenticated, and core drops a value it cannot read rather
12
+ * than refusing the find, so a wrong one can never break it.
13
+ */
14
+ export const CALLER_SOURCE_HEADER = 'x-one-source';
15
+ export const CALLER_AGENT_HEADER = 'x-one-agent';
16
+ export const CALLER_MODEL_HEADER = 'x-one-agent-model';
17
+ export function callerHeaders(caller = {}) {
18
+ const headers = { [CALLER_SOURCE_HEADER]: 'cli' };
19
+ const name = caller.client?.name?.trim();
20
+ if (name) {
21
+ const version = caller.client?.version?.trim();
22
+ headers[CALLER_AGENT_HEADER] = version ? `${name}/${version}` : name;
23
+ }
24
+ const model = caller.model?.trim();
25
+ if (model)
26
+ headers[CALLER_MODEL_HEADER] = model;
27
+ return headers;
28
+ }
@@ -0,0 +1,17 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { callerHeaders } from './caller.js';
4
+ describe('callerHeaders', () => {
5
+ it('always names the source, and nothing else when nothing is known', () => {
6
+ assert.deepEqual(callerHeaders(), { 'x-one-source': 'cli' });
7
+ });
8
+ it('names the MCP client with its version, as core reads it', () => {
9
+ assert.equal(callerHeaders({ client: { name: 'claude-code', version: '2.4.1' } })['x-one-agent'], 'claude-code/2.4.1');
10
+ assert.equal(callerHeaders({ client: { name: 'cursor' } })['x-one-agent'], 'cursor');
11
+ assert.equal(callerHeaders({ client: { name: ' ', version: '1.0' } })['x-one-agent'], undefined);
12
+ });
13
+ it('names the model only when the agent gave one', () => {
14
+ assert.equal(callerHeaders({ model: ' claude-sonnet-5 ' })['x-one-agent-model'], 'claude-sonnet-5');
15
+ assert.equal(callerHeaders({ model: ' ' })['x-one-agent-model'], undefined);
16
+ });
17
+ });
package/build/client.js CHANGED
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import axios from 'axios';
10
10
  import FormData from 'form-data';
11
+ import { callerHeaders } from './caller.js';
11
12
  import { fetchPaginatedData, replacePathVariables } from './helpers.js';
12
13
  /** Header names that only the server may set on an outgoing request. */
13
14
  const RESERVED_HEADER_PREFIX = 'x-one-';
@@ -190,12 +191,14 @@ export class OneClient {
190
191
  * @param task - The whole task in one line, which helps break ties
191
192
  * @param knowledgeAgent - Search the catalog for writing integration code
192
193
  * rather than the one for executing actions
194
+ * @param caller - The MCP client that connected and the model it named,
195
+ * sent so One records who asked
193
196
  * @returns One answer per intent, in the order asked
194
197
  * @throws {Error} Naming core's reason when it refuses the request
195
198
  */
196
- async findActions(requests, task, knowledgeAgent) {
199
+ async findActions(requests, task, knowledgeAgent, caller = {}) {
197
200
  try {
198
- const response = await axios.post(`${this.baseUrl}/v1/available-actions/find`, { requests, ...(task ? { task } : {}), knowledgeAgent }, { headers: this.generateHeaders() });
201
+ const response = await axios.post(`${this.baseUrl}/v1/available-actions/find`, { requests, ...(task ? { task } : {}), knowledgeAgent }, { headers: { ...this.generateHeaders(), ...callerHeaders(caller) } });
199
202
  return response.data || [];
200
203
  }
201
204
  catch (error) {
package/build/find.js CHANGED
@@ -37,7 +37,7 @@ export class FindArgsError extends Error {
37
37
  */
38
38
  export async function findOneActions(args, deps, settings) {
39
39
  if (args.requests && !args.load) {
40
- return find(args.requests, args.task, deps, settings);
40
+ return find(args.requests, args.task, deps, settings, { client: settings.client, model: args.ai_model });
41
41
  }
42
42
  if (args.load && !args.requests && args.task === undefined) {
43
43
  return load(args.load, deps, settings);
@@ -141,12 +141,12 @@ function finish(sections, documented, settings) {
141
141
  return body;
142
142
  return `${body}\n\n---\n\n${settings.knowledgeAgent ? codeGuideRules() : FIND_EXECUTE_HINT}`;
143
143
  }
144
- async function find(requests, task, deps, settings) {
144
+ async function find(requests, task, deps, settings, caller) {
145
145
  if (requests.length === 0 || requests.length > FIND_MAX_INTENTS) {
146
146
  throw new FindArgsError(`Send between 1 and ${FIND_MAX_INTENTS} requests.`);
147
147
  }
148
148
  const reachable = requests.filter((r) => reaches(settings, r.platform));
149
- const answers = reachable.length > 0 ? await deps.findActions(reachable, task, settings.knowledgeAgent) : [];
149
+ const answers = reachable.length > 0 ? await deps.findActions(reachable, task, settings.knowledgeAgent, caller) : [];
150
150
  let next = 0;
151
151
  const plans = requests.map((intent) => {
152
152
  if (!reaches(settings, intent.platform)) {
@@ -37,10 +37,13 @@ const settings = (overrides = {}) => ({
37
37
  /** Core answering `answers` in turn, and raw knowledge per action. */
38
38
  function deps(answers, knowledge = {}) {
39
39
  const asked = [];
40
+ const callers = [];
40
41
  return {
41
42
  asked,
42
- async findActions(requests) {
43
+ callers,
44
+ async findActions(requests, _task, _knowledgeAgent, caller) {
43
45
  asked.push(requests);
46
+ callers.push(caller);
44
47
  return answers.slice(0, requests.length);
45
48
  },
46
49
  async getActionDetails(id) {
@@ -60,6 +63,11 @@ function deps(answers, knowledge = {}) {
60
63
  }
61
64
  const intent = [{ platform: 'gmail', intent: 'send an email' }];
62
65
  describe('find_one_actions', () => {
66
+ it('tells core which client and model asked', async () => {
67
+ const found = deps([answer()]);
68
+ await findOneActions({ requests: intent, ai_model: 'claude-sonnet-5' }, found, settings({ client: { name: 'claude-code', version: '2.4.1' } }));
69
+ assert.deepEqual(found.callers, [{ client: { name: 'claude-code', version: '2.4.1' }, model: 'claude-sonnet-5' }]);
70
+ });
63
71
  it('takes requests or load, never both, never neither, and no task with a load', async () => {
64
72
  for (const args of [{}, { requests: intent, load: [{ action_id: 'a' }] }, { load: [{ action_id: 'a' }], task: 't' }]) {
65
73
  await assert.rejects(findOneActions(args, deps([answer()]), settings()), (e) => e instanceof FindArgsError && e.message === FIND_ARGS_ERROR);
package/build/index.js CHANGED
@@ -123,6 +123,7 @@ async function handleFindOneActions(args) {
123
123
  ? null
124
124
  : oneClient.getUserConnections().map(c => c.platform),
125
125
  baseUrl: oneClient.getBaseUrl(),
126
+ client: server.server.getClientVersion(),
126
127
  });
127
128
  return {
128
129
  content: [
package/build/schemas.js CHANGED
@@ -21,14 +21,14 @@ export const findOneActionsInputSchema = {
21
21
  platform: z.string().describe('The kebab-case platform identifier (e.g. "gmail", "stripe").'),
22
22
  intent: z.string().describe('The operation alone, in a few words (e.g. "send a message to a channel"), without its data: no IDs, names or message text.')
23
23
  })).min(1).max(10).optional().describe('What to do, one entry per API operation, on any platforms (1 to 10). Each is the kebab-case platform (e.g. "gmail", "hubspot") and a short intent (e.g. "send an email", "find a contact by email").'),
24
- task: z.string().optional().describe('The whole task in one line, in general terms (e.g. "email a report to a contact"): what it does, without names, addresses, IDs or message text. It helps choose between similar actions. Never the chat history. Only with `requests`.'),
24
+ task: z.string().optional().describe('The whole task in one line, in general terms (e.g. "email a report to a contact"): what it does, without names, addresses, IDs or message text. Send it with every find: it helps tell similar actions apart. Never the chat history. Only with `requests`.'),
25
25
  load: z.array(z.object({
26
26
  action_id: z.string().describe("The actionId from a find_one_actions answer."),
27
27
  section: z.string().optional().describe('Section name(s) to load, comma-separated (e.g. "Response Fields"). Omit, with `full` and `toc` unset, for the digest. In knowledge mode every load returns the whole document, so `section` and `toc` do not narrow it.'),
28
28
  full: z.boolean().optional().describe("Load the whole document. Ignored when `section` is set."),
29
29
  toc: z.boolean().optional().describe("Load only the table of contents. Ignored when `section` is set.")
30
30
  })).min(1).max(10).optional().describe("More documentation for actions already found (1 to 10): a section the digest omitted, the whole document, the table of contents, or an alternative's documentation. Use instead of `requests`."),
31
- ai_model: z.string().optional().describe('The AI model you are running as (e.g. "claude-sonnet-5", "gpt-5"), which helps optimize the documentation returned. Optional.')
31
+ ai_model: z.string().optional().describe('Your model name as you know it (e.g. "claude-sonnet-5", "gpt-5"), recorded with this find. Send it with every find; leave it out only if you do not know which model you are.')
32
32
  };
33
33
  /**
34
34
  * Schema for executing One actions
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withone/mcp",
3
- "version": "2.0.0",
3
+ "version": "2.1.1",
4
4
  "description": "A Model Context Protocol Server for One",
5
5
  "type": "module",
6
6
  "bin": {