Skip to content

Commit 25812d1

Browse files
committed
docs: add a 2.x to 3.0 migration guide
Every breaking change since 2.10.0, each with the 2.x code and its 3.0 replacement: removed exports, constructor options, server-side search and execute, the result shape and download links, the header-argument allowlist, account discovery and ordering, submitFeedback(), the error hierarchy and StackOneAPIError messages, schema pass-through, and hand-built tools. Structured like the Python SDK's MIGRATION.md so the two read the same. Linked from the top of the README.
1 parent 3da22ec commit 25812d1

2 files changed

Lines changed: 302 additions & 0 deletions

File tree

‎MIGRATION.md‎

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
# Migrating from 2.x to 3.0
2+
3+
3.0 is a thin client over StackOne's MCP endpoint. Every tool call goes over MCP `tools/call`, and the only other request the SDK makes is `GET /accounts`, to find your linked accounts. Search runs on the server. Anything MCP does not support has been removed rather than kept on a second transport.
4+
5+
Work through the sections that apply to you. Each one gives the 2.x code and its 3.0 replacement.
6+
7+
- [Installation](#installation)
8+
- [Removed exports](#removed-exports)
9+
- [Constructor options](#constructor-options)
10+
- [Search and execute](#search-and-execute)
11+
- [Results](#results)
12+
- [Tool arguments and headers](#tool-arguments-and-headers)
13+
- [Accounts](#accounts)
14+
- [Feedback](#feedback)
15+
- [Errors](#errors)
16+
- [Schemas given to a model](#schemas-given-to-a-model)
17+
- [Hand-built tools and `ExecuteConfig`](#hand-built-tools-and-executeconfig)
18+
19+
## Installation
20+
21+
`zod` is no longer a peer dependency, and `@orama/orama` and `defu` are no longer dependencies. Nothing else changes.
22+
23+
```bash
24+
# 2.x
25+
npm install @stackone/ai zod
26+
27+
# 3.0
28+
npm install @stackone/ai
29+
```
30+
31+
## Removed exports
32+
33+
These names are no longer exported from `@stackone/ai`:
34+
35+
| Removed | Use instead |
36+
| ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
37+
| `SearchTool`, `SearchMode`, `SearchConfig`, `SearchToolsOptions`, `SearchActionNamesOptions` | `toolset.search()`. See [Search and execute](#search-and-execute) |
38+
| `SemanticSearchClient`, `SemanticSearchError`, `SemanticSearchOptions`, `SemanticSearchResponse`, `SemanticSearchResult` | `toolset.search()`, which returns `SearchResult` objects |
39+
| `createFeedbackTool` | `toolset.submitFeedback()`. See [Feedback](#feedback) |
40+
| `DefenderConfig`, `DefenderMode`, `DEFAULT_DEFENDER_CONFIG` | Defender settings in the StackOne dashboard. See [Constructor options](#constructor-options) |
41+
| `BinaryDownloadResult`, `isBinaryDownloadResult` | The download link in the result. See [Results](#results) |
42+
| `AuthenticationConfig`, `BaseToolSetConfig` | `StackOneToolSetConfig`, with `apiKey` |
43+
| `ParameterLocation` | Nothing. The server maps arguments onto the request |
44+
45+
`ToolSetError`, `ToolSetConfigError` and `ToolSetLoadError` are still exported from the package root.
46+
47+
## Constructor options
48+
49+
**An API key is required.** 2.x warned and carried on without one; 3.0 throws `ToolSetConfigError`. The `strict` option, which made 2.x throw, is gone.
50+
51+
**`authentication`, `rpcClient`, `search`, `strict` and `defender` are removed.** Pass the key as `apiKey` (or set `STACKONE_API_KEY`), and configure Defender per project in the StackOne dashboard. The `defenderMode` getter is gone with the option.
52+
53+
```typescript
54+
// 2.x
55+
const toolset = new StackOneToolSet({
56+
authentication: { type: 'basic', credentials: { username: apiKey } },
57+
search: { method: 'auto' },
58+
defender: { ...DEFAULT_DEFENDER_CONFIG, blockHighRisk: true },
59+
});
60+
61+
// 3.0
62+
const toolset = new StackOneToolSet({ apiKey });
63+
```
64+
65+
**`Authorization`, `x-account-id` and `User-Agent` in `headers` are ignored**, with a warning. The SDK always sets them itself, so a `headers` option can no longer replace the credential or point requests at another account. Use `apiKey` and `accountId` / `accountIds` instead. Any other header is still sent with every request.
66+
67+
```typescript
68+
// 2.x: this header replaced the Basic credential
69+
new StackOneToolSet({ headers: { Authorization: `Bearer ${token}`, 'x-account-id': 'acc-1' } });
70+
71+
// 3.0
72+
new StackOneToolSet({ apiKey, accountId: 'acc-1' });
73+
```
74+
75+
## Search and execute
76+
77+
Client-side search is gone: the local BM25/TF-IDF index, the semantic search client, the `search` constructor option, and the `tool_search` / `tool_execute` meta tools. The server's `*_search_actions` tool now does the search, and `toolset.execute()` runs an action by id through the server's `*_execute_action` tool.
78+
79+
```typescript
80+
// 2.x
81+
const toolset = new StackOneToolSet({ search: { method: 'auto' } });
82+
const tools = await toolset.searchTools('list employees', { topK: 5 });
83+
const result = await tools.toArray()[0]?.execute({ query_page_size: 25 });
84+
85+
const names = await toolset.searchActionNames('time off requests', { topK: 5 });
86+
87+
// 3.0
88+
const toolset = new StackOneToolSet();
89+
const [hit] = await toolset.search('list employees', { topK: 5 }); // SearchResult[], best first
90+
const result = await toolset.execute(
91+
hit.action_id,
92+
{ query: { page_size: 25 } }, // the nested form hit.input_schema describes
93+
{ sessionId: hit.session_id },
94+
);
95+
```
96+
97+
`searchTools()`, `searchActionNames()`, `getSearchTool()`, `getSearchConfig()` and `getTools()` are removed. `search()` returns plain objects carrying `action_id`, plus `description`, `similarity_score`, `input_schema`, `example_request` and `session_id` when the server sends them. `execute()` raises rather than returning `{ error }`: `ToolSetLoadError` when no linked connector serves the action, `StackOneAPIError` when the action fails.
98+
99+
To give a model the search and execute tools, set the tool mode on the toolset. `openai()` no longer takes `mode`.
100+
101+
```typescript
102+
// 2.x
103+
const toolset = new StackOneToolSet({ search: {} });
104+
const openAITools = await toolset.openai({ mode: 'search_and_execute' });
105+
106+
// 3.0: two tools per connector, served by the server
107+
const toolset = new StackOneToolSet({ toolMode: 'search_execute' });
108+
const tools = await toolset.fetchTools();
109+
const openAITools = tools.toOpenAI();
110+
// ...then run the model's tool calls and get the messages to send back:
111+
messages.push(message, ...(await tools.executeOpenAIToolCalls(message.tool_calls)));
112+
```
113+
114+
## Results
115+
116+
**Every tool returns the server's result as the server wrote it.** For an action, that is `{ isError: false, result, defenderMetadata?, policyMetadata? }`. Search results are bare JSON. This applies to `tool.execute()` and `toolset.execute()` alike.
117+
118+
```typescript
119+
const tool = (await toolset.fetchTools()).getTool('hibob_list_employees');
120+
121+
// 2.x
122+
const employees = (await tool.execute({})).data;
123+
124+
// 3.0
125+
const employees = (await tool.execute({})).result.data;
126+
```
127+
128+
A result with `isError` set raises `StackOneAPIError`, with the status from its payload in `statusCode` and the payload in `responseBody`.
129+
130+
**File actions return a download link, not bytes.** The SDK does not follow the link. When no link can be issued, the call raises `StackOneAPIError` with `statusCode` 501.
131+
132+
```typescript
133+
// 2.x
134+
const result = await download.execute({ id: 'file-id' });
135+
if (isBinaryDownloadResult(result)) {
136+
writeFileSync(result.fileName ?? 'download.bin', result.content);
137+
}
138+
139+
// 3.0
140+
const { result: link } = await download.execute({ id: 'file-id' });
141+
// { download_url, expires_at, file: { name, content_type, content_length } }
142+
const name = path.basename(link.file.name ?? 'download.bin'); // chosen by the provider: keep only the basename
143+
writeFileSync(name, Buffer.from(await (await fetch(link.download_url)).arrayBuffer()));
144+
```
145+
146+
**`dryRun` describes the `tools/call`**, not an HTTP request:
147+
148+
```typescript
149+
await tool.execute({ id: '1' }, { dryRun: true });
150+
// 2.x: { url: '.../actions/rpc', method: 'POST', headers, body, mappedParams }
151+
// 3.0: { url: '.../mcp', method: 'tools/call', name: 'hibob_get_employee', arguments: { id: '1' } }
152+
```
153+
154+
## Tool arguments and headers
155+
156+
**Arguments are sent exactly as given**, as `tools/call` arguments. The SDK no longer splits flat `path_` / `query_` / `body_` keys into an `/actions/rpc` envelope; the server maps them itself.
157+
158+
**`fetchTools()` tools have the server's own argument shape.** 2.x asked the server for the flat, prefixed style (`?param-style=flat_prefixed`). That request is gone, so the argument names are whatever the server serves for your project. Read them from `tool.parameters.properties` rather than hard-coding them:
159+
160+
```typescript
161+
// 2.x
162+
await tool.execute({ body_variables: { first: 25 } });
163+
164+
// 3.0: use the keys the schema names, for example
165+
console.log(tool.parameters.properties);
166+
await tool.execute({ body: { variables: { first: 25 } } });
167+
```
168+
169+
**Header arguments are allowlisted.** A header argument is an entry of a `headers` object argument, or a top-level `headers_<name>` argument. Each one is forwarded only if the tool's schema declares it in the same form: under `headers.properties`, or as a `headers_<name>` property. An open `headers` object, with no `properties`, declares every name. `Authorization`, `x-account-id` and `User-Agent` are never forwarded, even when declared, because the SDK sets them itself. Anything dropped is logged as a warning. Every other argument is sent unchanged.
170+
171+
`*_execute_action` serves an open `headers` object, so `toolset.execute()` passes your own headers on to the action, with the exception of those three:
172+
173+
```typescript
174+
await toolset.execute('linear_list_comments', { headers: { 'x-request-id': 'abc' } });
175+
```
176+
177+
## Accounts
178+
179+
**With no account id, the SDK discovers your accounts.** In 2.x, calling `fetchTools()` with no account listed tools without an `x-account-id`, which the API refuses. In 3.0 it asks `GET /accounts` (also available as `toolset.fetchAccounts()`) and lists the catalog of every active account. If you have many accounts, pass `accountId`, `accountIds` or call `setAccounts()` so the SDK does not fetch every catalog.
180+
181+
**Listings are merged in sorted account order.** When two accounts serve the same tool name, `getTool()` returns the first one listed — now the one on the lowest account id, where 2.x followed the order you passed. A warning names the clashing tools. Pass `accountIds` to choose the account yourself.
182+
183+
**`fetchTools()` returns fresh tool instances on every call**, never the cached `Tools`, so `setAccountId()` on one tool no longer changes what later callers get. An account whose listing fails is skipped with a warning, unless every account fails.
184+
185+
## Feedback
186+
187+
The client-side `tool_feedback` tool and `createFeedbackTool()` have been removed, and `fetchTools()` no longer appends a feedback tool of its own.
188+
189+
```typescript
190+
// 2.x
191+
const feedbackTool = (await toolset.fetchTools()).getTool('tool_feedback');
192+
await feedbackTool.execute({
193+
feedback: 'Worked well',
194+
account_id: 'acc_123',
195+
tool_names: ['workday_list_workers'],
196+
});
197+
198+
// 3.0
199+
const [hit] = await toolset.search('list workers');
200+
await toolset.execute(hit.action_id, {}, { sessionId: hit.session_id });
201+
await toolset.submitFeedback({
202+
rating: 'positive',
203+
toolNames: [hit.action_id],
204+
feedback: 'Worked well',
205+
sessionId: hit.session_id,
206+
});
207+
```
208+
209+
When feedback is enabled for your project, the server also serves a `stackone_submit_feedback` tool. **`fetchTools()` and `openai()` include it**, once, however many accounts are linked. If you don't want a model to call it, filter it out:
210+
211+
```typescript
212+
const tools = (await toolset.fetchTools()).filter(
213+
(tool) => tool.name !== 'stackone_submit_feedback',
214+
);
215+
```
216+
217+
`submitFeedback()` throws `ToolSetLoadError` when feedback is not enabled.
218+
219+
## Errors
220+
221+
**Every error the SDK throws is a `StackOneError`.** `ToolSetError`, `ToolSetConfigError` and `ToolSetLoadError` extended `Error` in 2.x; they now extend `StackOneError`, alongside `StackOneAPIError`. If you tell them apart with `instanceof`, check the toolset errors first:
222+
223+
```typescript
224+
try {
225+
await toolset.fetchTools();
226+
} catch (error) {
227+
if (error instanceof ToolSetError) {
228+
// configuration or loading — test this first: it is also a StackOneError now
229+
} else if (error instanceof StackOneError) {
230+
// the API refused the request
231+
}
232+
}
233+
```
234+
235+
**`StackOneAPIError` keeps the message it is given.** 2.x appended the response body's `message` to it (`Request failed: path.id is missing`). Read the server's explanation from `error.responseBody` instead; the SDK's own messages already include it.
236+
237+
```typescript
238+
// 2.x
239+
new StackOneAPIError('Request failed', 400, { message: 'path.id is missing' }).message;
240+
// 'Request failed: path.id is missing'
241+
242+
// 3.0
243+
new StackOneAPIError('Request failed', 400, { message: 'path.id is missing' }).message;
244+
// 'Request failed'
245+
```
246+
247+
## Schemas given to a model
248+
249+
**`toJsonSchema()` passes the served schema through**: root keywords such as `$schema`, `$defs`, `title` and `additionalProperties` reach the model as the server served them, and `required` is the served list, left out when it is missing or empty. `toOpenAI()`, `toAnthropic()`, `toOpenAIResponses()`, `toAISDK()` and `toClaudeAgentSdkTool()` fold a top-level `oneOf` / `anyOf` / `allOf`, which those APIs reject, into the root.
250+
251+
## Hand-built tools and `ExecuteConfig`
252+
253+
**`BaseTool` no longer makes HTTP requests.** Its `execute()` throws unless a subclass overrides it, `RequestBuilder` is gone, and `ExecuteConfig` is `{ kind: 'mcp', … }` or `{ kind: 'local', … }` — the `http` and `rpc` kinds are removed. Tools from `fetchTools()` execute over MCP.
254+
255+
```typescript
256+
// 2.x
257+
const tool = new BaseTool(
258+
'get_employee',
259+
'Get an employee',
260+
{ type: 'object', properties: { id: { type: 'string' } } },
261+
{
262+
kind: 'http',
263+
method: 'GET',
264+
url: 'https://api.example.com/employees/{id}',
265+
bodyType: 'json',
266+
params: [{ name: 'id', location: 'path', type: 'string' }],
267+
},
268+
{ Authorization: `Bearer ${token}` },
269+
);
270+
271+
// 3.0
272+
class GetEmployee extends BaseTool {
273+
override async execute(input?: JsonObject | string): Promise<JsonObject> {
274+
const { id } = typeof input === 'string' ? JSON.parse(input) : (input ?? {});
275+
const response = await fetch(`https://api.example.com/employees/${id}`, {
276+
headers: { Authorization: `Bearer ${token}` },
277+
});
278+
return response.json();
279+
}
280+
}
281+
const tool = new GetEmployee(
282+
'get_employee',
283+
'Get an employee',
284+
{ type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
285+
{ kind: 'local' },
286+
);
287+
```
288+
289+
**Tools no longer carry headers.** The `headers` constructor argument, `getHeaders()` and `setHeaders()` are removed from `BaseTool`, and `ToolExecution` — the `execution` metadata `toAISDK()` can attach — no longer has `headers`, so it cannot leak the credential. `StackOneTool`'s fifth constructor argument is now the account id; use `getAccountId()` / `setAccountId()` to read or rebind it.
290+
291+
**`BaseTool#connector` and `Tools#getConnectors()` are removed.** Take the provider from the tool name, or filter with `fetchTools({ providers })`:
292+
293+
```typescript
294+
// 2.x
295+
const connectors = tools.getConnectors();
296+
297+
// 3.0
298+
const connectors = new Set(tools.map((tool) => tool.name.split('_')[0]));
299+
```

‎README.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66

77
<!-- DeepWiki badge generated by https://deepwiki.ryoppippi.com/ -->
88

9+
> **Upgrading from 2.x?** 3.0 has breaking changes. See the
10+
> [migration guide](https://github.com/StackOneHQ/stackone-ai-node/blob/main/MIGRATION.md).
11+
912
## StackOneToolSet
1013

1114
The StackOne AI SDK provides the `StackOneToolSet` class, a thin client over StackOne's MCP (Model Context Protocol) endpoint. It lists the tools your linked accounts serve and executes them, handing your model each tool's schema exactly as the server served it.

0 commit comments

Comments
 (0)