|
| 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 | +``` |
0 commit comments