refactor!: MCP-only toolset — every tool over tools/call, with search/execute sessions and submitFeedback() - #383
willleeney wants to merge 29 commits into
Conversation
commit: |
There was a problem hiding this comment.
Review completed against the latest diff
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
Mirrors the same removal in stackone-ai-python. Search is absent from the
sdk-conformance wire contract — the mock serves no /actions/search and no runner
exercises any search method — and the contract docs flag client-side search as
something that belongs server-side.
BREAKING: removes searchTools(), searchActionNames(), getSearchTool(), the
SearchTool class, the tool_search/tool_execute meta tools, openai({ mode:
'search_and_execute' }), the StackOneToolSet `search` option, and the
SearchMode / SearchConfig / SearchToolsOptions / SearchActionNamesOptions types.
Scope the catalog with `providers` / `actions` on fetchTools() instead.
Deletes semantic-search.ts, local-search.ts, utils/tfidf-index.ts and
utils/normalize.ts, and drops the now-unused @orama/orama dependency and its
catalog entry.
Methods were removed by brace-matching rather than line ranges, and knip caught
two leftovers the grep missed: an unresolved `normalizeActionName` import in
toolsets.ts and the orphaned DEFAULT_HYBRID_ALPHA constant.
255 tests pass, no type errors, lint and knip clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fc3752a deleted buildTools but left two callers, so getTools() and openai({ mode: 'search_and_execute' }) would have thrown "this.buildTools is not a function" at runtime. Both were meta-tool entry points with nothing behind them once search was removed, so both are gone. Also drops scripts/benchmark-search.ts, which benchmarked the removed search API, and an unused SearchTool import in toolsets.test.ts. None of this was caught because the typechecker was not running: vitest spawns tsc, the flake supplies typescript-go, and these commands were run outside `nix develop`. Vitest reported "Type Errors: no errors" while also printing "spawn tsc ENOENT" — so a typecheck that never ran looked like a clean one. `tsc --noEmit` finds all four the moment it can actually execute. Verified with a one-off typescript: typecheck clean, 255 tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Defender is configured per project in the StackOne dashboard now, so the SDK no longer carries its own copy of the setting or sends one with each RPC call. Dropping it removes a second source of truth that could silently override what the dashboard shows. - Remove the `defender` constructor option, the `defenderMode` getter and the once-per-process override warning. - Stop sending `defender_config` in the `/actions/rpc` payload. - Remove the defender-config example and its tests, and the README section. BREAKING CHANGE: `DefenderConfig`, `DefenderMode` and `DEFAULT_DEFENDER_CONFIG` are no longer exported, the `defender` option of `StackOneToolSetConfig` and `StackOneToolSet#defenderMode` are removed, and RPC requests no longer carry `defender_config`. Configure Defender in the StackOne dashboard.
`fetchTools()` appended a `tool_feedback` tool that the SDK built itself and posted to `/ai/tool-feedback`. The server now serves a feedback tool of its own, `stackone_submit_feedback`, only when feedback is enabled for the project. A client-side stand-in reports success for feedback that goes nowhere when the project has it off, and it skewed every tool count by one. BREAKING CHANGE: `createFeedbackTool` is no longer exported and `fetchTools()` no longer appends a `tool_feedback` tool. Use the server-served `stackone_submit_feedback` tool (or the toolset's `submitFeedback()` method, added later in this series).
The RPC tool merged the envelope's headers OVER its own with
`defu(extraHeaders, baseHeaders)`. Tool arguments are model-controlled, so
a call carrying `headers_x-account-id` (or a nested `headers` object)
replaced the account the tool was fetched for, both in the envelope and in
the `x-account-id` HTTP header the API reads: a prompt injection could run
any action against any tenant the API key reaches.
- Filter model-supplied headers through an allowlist built from the served
schema's own `headers_*` properties, compared trimmed and
case-insensitively, and reject values carrying CR/LF or other control
characters.
- Apply the SDK's own headers after the merge, so `x-account-id` is always
the tool's account.
- Serve the mock catalog through a low-level MCP `Server`. `McpServer`
expects a Zod shape and listed every JSON Schema as `properties: {}`, so
the tests never saw a declared parameter.
Regression tests cover the flat and nested injection paths and a declared
header that must survive.
BREAKING CHANGE: headers a tool call supplies are dropped unless the tool's
served schema declares them as a `headers_<name>` property. Undeclared
headers were previously forwarded in the RPC envelope.
`ToolSetError`, `ToolSetConfigError` and `ToolSetLoadError` extended `Error` directly, so `error instanceof StackOneError` missed the two errors a user is most likely to hit first: a missing API key and a catalog that will not load. They now extend `StackOneError`, alongside `StackOneAPIError`, and live in `src/utils/error-toolset.ts`. BREAKING CHANGE: `ToolSetError` (and so `ToolSetConfigError` and `ToolSetLoadError`) now extends `StackOneError`. Code that distinguished the two with `instanceof StackOneError` must check the toolset errors first.
`fileName` on a download result came straight from the server's Content-Disposition, which whoever uploaded the file to the connected provider chooses. A caller that writes the download to `result.fileName` — the obvious thing to do — could be made to write `../../.ssh/authorized_keys` or `/etc/cron.d/x`, and the RFC 5987 branch percent-decoded, so `%2e%2e%2f` got through any filter applied earlier. Port the Python SDK's `_safe_basename`, applied last, after decoding: - treat `/`, `\` and `:` as separators (covers `C:evil.exe` and NTFS alternate data streams such as `report.pdf:payload`); - strip control and Unicode format characters (the U+202E extension spoof); - return null for an empty name, `.` or `..`; - cap at 255 UTF-8 bytes on a character boundary, keeping the extension. Parameters are now matched at a parameter boundary (`notfilename=` is ignored), and the extended form decodes with its declared charset (ISO-8859-1 included), falling back to UTF-8 for an unknown label, as the Python SDK does. The Python version leaves a name with no dot uncapped and does not treat `:` inside a name as a separator; both are handled here.
Port the Python SDK 3.0 toolset. Tools are listed from the MCP endpoint
per account and executed over `/actions/rpc`, or over MCP `tools/call` for
the tools with no action behind them. Schemas are passed through as
served.
Listing and accounts
- An API key alone is enough: with no account configured, `fetchTools()`
discovers the key's active accounts via `GET /accounts` (now public as
`fetchAccounts()`). It used to send an unscoped request the API refuses.
- Accounts are listed concurrently, at most 10 at a time, with
`param-style=flat_prefixed` pinned and pagination followed. A failing
account is skipped with a warning unless every account fails.
- The cache holds listings, not tools: every call builds fresh tools on a
deep copy of the schema, so one caller can no longer rescope or mutate
another's. The key covers the account scope, mode, base URL and API key,
and a generation guard stops an in-flight listing from repopulating the
cache after `clearCatalogCache()`. Degraded listings are not cached.
- `toolMode` / `fetchTools({ mode })` select `search_execute`, whose meta
tools execute over `tools/call`. `stackone_submit_feedback` executes
over `tools/call` in every mode and is returned once, however many
accounts list it.
- Provider filtering matches a full prefix (`browser_linkedin`), action
globs follow `fnmatch`, and a name served by two accounts warns.
Execution
- RPC arguments are split into the envelope by their flat_prefixed
location, decided once from the served schema: only when ALL declared
keys are prefixed (otherwise a warning). A reserved container key given
a scalar is rejected; prototype-named fields are plain data.
- Headers are allowlisted from the served `headers_*` properties on both
the RPC and MCP paths. `Authorization`, `x-account-id` and `User-Agent`
are set by the SDK after every merge, including the `headers` option.
- Every MCP call (listing and `tools/call`) and every RPC call honours
`timeout`. MCP transport failures become `StackOneAPIError` with the
HTTP status (e.g. 412 for a dead account); an `isError` result raises
with the status from its payload. API errors lead with the server's own
message and keep status and body.
Schemas
- `toJsonSchema()` is the served root verbatim (`$schema`, `$defs`,
`title`, `additionalProperties`, `oneOf`, ...) with the served
`required`. No internal marker is needed, so nothing named `nullable` is
ever touched.
- `toOpenAI()`, `toAnthropic()`, `toOpenAIResponses()`, `toAISDK()` and
`toClaudeAgentSdkTool()` fold a top-level `oneOf`/`anyOf`/`allOf`
(which OpenAI and Anthropic reject) into the root. Strict Responses and
the AI SDK close the root with `additionalProperties: false`.
Mocks refuse an unscoped `/mcp` or `/actions/rpc` request (400) and an
unknown account (404), serve schemas verbatim, `GET /accounts`, the
search/execute meta tools with a `session_id`, and optionally
`stackone_submit_feedback`. `defu` and the `zod` peer are no longer used.
BREAKING CHANGE: `RequestBuilder` and direct-HTTP execution are removed:
`ExecuteConfig` no longer has an `http` kind, `ParameterLocation` is no
longer exported, and `BaseTool#execute` throws unless overridden.
`BaseTool` no longer takes a `headers` constructor argument and loses
`getHeaders()`/`setHeaders()` and `connector`; `Tools#getConnectors()` is
removed. `StackOneToolSetConfig` loses `authentication`, `strict` and
`rpcClient`, and `AuthenticationConfig` and `BaseToolSetConfig` are no
longer exported. The constructor throws `ToolSetConfigError` without an
API key. `fetchTools()` returns fresh tool instances on every call (never
the cached `Tools`), ordered by account id, and discovers accounts when
none is configured. `ToolExecution` no longer carries `headers`. An RPC
timeout is a `StackOneError` rather than a `StackOneAPIError` with status
0, and `StackOneAPIError` no longer appends `responseBody.message` to the
message it is given. A `dryRun` result's `headers` are the HTTP request
headers, without `Authorization`.
The recommended surface: find an action in natural language, run it by
id, and record how it went, without loading a tool catalog into a model's
context. Ported from the Python SDK 3.0, plus the Node side of the
feedback/session contract.
- `search(query, { topK, accountIds })` queries every linked connector's
`*_search_actions` meta tool (`tool-mode=search_execute`) concurrently
and ranks the hits globally by `similarity_score`, rather than leaving
them grouped by whichever connector answered first. `topK` is validated
1..50 before any round trip. A connector that fails is skipped with a
warning unless all do. Every hit carries the `session_id` of the search
that produced it, when the server issued one.
- `execute(actionId, args?, { sessionId, accountIds })` routes to the
longest connector whose name prefixes the action id, stripping the
account id from the meta tool's name by identity (nanoid ids contain
`_`). `session_id` is forwarded as a top-level argument only when given,
and `action_id` is pinned last, so a model-supplied `action_id` in the
arguments cannot swap the action. The `{ isError, result }` wrapper is
unwrapped, and an `isError` result raises.
- `submitFeedback({ rating, toolNames, feedback?, category?, sessionId?,
source?, accountIds? })` finds the server-served
`stackone_submit_feedback` in the catalog (any mode) and calls it over
`tools/call` with snake_case arguments, omitting unset keys rather than
sending null. When the server does not serve it, it raises
`ToolSetLoadError`: feedback is not enabled for the project.
The counterpart to `toOpenAI()`, ported from the Python SDK's `execute_openai_tool_calls`: run a Chat Completions response's tool calls and get back the `tool` messages to send to the model, in order. A failed call does not throw. The error, and the server's response body when there is one, become the message content, so the model can read why and retry; an unknown tool or a non-function call is reported the same way. Errors that are not the SDK's still propagate. File bytes are base64-encoded rather than serialised as a byte array, as the Claude Agent SDK handler now does too.
- Add a search-and-execute example (accounts, ranked search, input schema, execute with the hit's session id, submitFeedback) and a test that runs the real example file against the mock API with nothing but an API key. - Complete the OpenAI example's tool-call loop with `executeOpenAIToolCalls()`, and show account discovery in the auth-management example. - README: lead with search/execute; document account discovery and `fetchAccounts()`, tool modes, feedback, what each adapter does to the served schema (including strict `additionalProperties: false`), the header allowlist, the error hierarchy, timeouts, safe download names and the new `dryRun` shape. Fix the `Tools#setAccountId` snippet, which never existed, and drop `zod` from the install line now it is not a peer. - Remove stale references left by the search removal: the Orama skill in CLAUDE.md and flake.nix, and `search-tools` in .env.example, which also now marks STACKONE_ACCOUNT_ID optional.
Mirror the Python SDK's conformance gate. The suite drives the built `dist` against a mock of the Unified Cloud API and grades the wire contract both SDKs share: RPC envelopes, schema pass-through (strict), account discovery and search/execute over `tools/call`. - `check-secret` lets fork PRs and Dependabot, which cannot read CONFORMANCE_REPO_TOKEN, skip the gate; everything else must pass it and fails if the token is missing or expired. - `conformance` checks out StackOneHQ/sdk-conformance pinned to a full SHA, builds the SDK and runs `pnpm test:node -- --strict-schema` with NODE_SDK_DIST pointed at the built dist. - `ci-ok` aggregates every job into the one status check to require, so renaming a job or adding a matrix entry cannot silently drop a check. There was no aggregate job before. Actions are pinned to full SHAs with version comments, as elsewhere.
Per-action tools posted to /actions/rpc, so the SDK split each tool
call into the RPC envelope itself and had to pin param-style so its
split matched the listed schema. Every tool now executes over tools/call
on the endpoint and account that listed it, and the server maps the
arguments with its own reverse map. The only request outside MCP is
GET /accounts for discovery.
A tool returns the server's result exactly as the server wrote it. For
an action tool, *_execute_action and feedback that is { isError: false,
result, defenderMetadata?, policyMetadata? }, while search results are
bare JSON. isError still raises StackOneAPIError. The unwrap that lived
only in execute() is gone, so execute() and tool.execute() return the
same shape for the same action. That unwrap also dropped defender
metadata.
The header allowlist reads a nested `headers` object as well as flat
headers_* properties, because the served style is no longer pinned.
BREAKING CHANGE: StackOneRpcTool, RpcExecuteConfig, BinaryDownloadResult
and isBinaryDownloadResult are removed. Action results are the server's
{ isError, result, ... } object rather than the /actions/rpc body. A
file action's `result` is the server's single-use { download_url,
expires_at, file } instead of bytes, and the call raises
StackOneAPIError with status 501 when no link can be issued. The /mcp
URL no longer pins param-style=flat_prefixed. dryRun returns
{ url, method: 'tools/call', name, arguments }.
The README described RPC execution, an RPC dryRun and byte downloads. It now shows the tools/call dryRun and the download link a file action returns, and says to reduce the provider-chosen filename before writing it.
StackOneHQ/sdk-conformance#5 grades execution over tools/call, forbids /actions/rpc (invariant 13) and grades results as the server wrote them. The previous pin still graded /actions/rpc envelopes, which this SDK no longer sends.
28cb186 to
7b14868
Compare
The root project type-checks tests/, and the example's e2e test imported it by a literal path, so tsc followed it into examples/search-and-execute.ts. Its '@stackone/ai' import resolves only when the examples workspace is installed, which the ai-peer-range jobs skip, so they failed. The test now imports the example by a computed URL. Also formats src/toolsets.search-execute.test.ts the way CI's oxfmt 0.23.0 does.
There was a problem hiding this comment.
0 issues found across 2 files (changes from recent commits).
Requires human review: Auto-approval blocked by 1 unresolved issue from previous reviews.
Re-trigger cubic
fd862c1 hid the example from the root typecheck by importing it through a computed URL, so tsc stopped checking it at all. Import it by its literal path again and map '@stackone/ai' to ./src/index.ts in the root tsconfig instead: the example now resolves the package to source whether or not the examples workspace is installed, which the ai-peer-range jobs skip. The three "'error' is of type 'unknown'" errors were fallout of the unresolved import, not defects in the example. The mapping only affects type resolution for files that import the package by name, which src never does, so the built dist and its published types are unchanged.
A header argument is an entry of a top-level `headers` object, or a top-level `headers_<name>` argument. Each is now forwarded only when the served schema declares it: `<name>` under `properties.headers.properties`, or the `headers_<name>` property itself. A `headers` property served as an object with no `properties` (as on every `*_execute_action`) declares any name, so `toolset.execute()` can now pass host-set headers such as `x-custom` through to the action. `Authorization`, `x-account-id` and `User-Agent` are never forwarded as header arguments, even when declared. Previously a `headers_<name>` argument went to tools/call unfiltered, and a flat `headers_<name>` declaration also let the nested `headers.<name>` through. Every other argument goes to tools/call unchanged. Each dropped header argument now warns with the real reason, "not declared by the schema" or "set by the SDK", instead of claiming every drop was undeclared. The tenant-switch regression test now asserts the arguments tools/call received, not only the transport's x-account-id. BREAKING CHANGE: a `headers_<name>` tool argument is dropped with a warning unless the served schema declares `headers_<name>`, and is never forwarded for `authorization`, `x-account-id` or `user-agent`. A nested `headers.<name>` is forwarded only when `properties.headers` declares it (or is an open object); a flat `headers_<name>` declaration no longer admits it.
submitFeedback() listed the catalog in the toolset's own mode across every scoped account. The feedback tool is global, so it now lists `tool-mode=search_execute` (two meta tools per connector rather than every action) on a single account and makes exactly one tools/call there. That account is the first in the order given, not the first by id: the first of the call's `accountIds`, else the toolset's, else the first active account `GET /accounts` lists. Account scoping keeps a separate, unsorted path for this; listing still sorts its scope for the cache key.
Two accounts can serve the same tool name. The Tools unit test pins first-match-wins; this pins it end to end: the listing is ordered by account id whatever order the caller named them in, so the duplicate getTool() returns is predictable.
Nothing has called it since the RPC client went: tools/call failures build their message from the result's own text in mcp-client.ts. Drop it and its tests, and stop the README promising the "400 Bad Request: path.id is missing" message format it produced.
…nt calls The discovered accounts were cached only once the request returned, so search() and fetchTools() started together on a fresh toolset each sent their own GET /accounts. Share the in-flight promise instead. A failed discovery is not kept, so the next call retries, and clearCatalogCache() drops an in-flight one as it already drops the result.
JSON.stringify(undefined) returns undefined, not a string, so a hand-built tool that returned nothing produced a tool message with `content: undefined` from executeOpenAIToolCalls(), and an empty text part from the Claude Agent SDK adapter. Write "null" instead, which is what the Python SDK sends for None (`json.dumps(None)`).
SearchResult typed only action_id and session_id, so reading a hit's score or schema needed a typeof guard, and passing `best.example_request` to execute() (as the README does) did not type-check. Declare `description`, `similarity_score`, `input_schema` and `example_request` as the per-connector `*_search_actions` tool serves them. All are optional: the server omits input_schema and example_request for an action with no inputs, and the SDK passes hits through without validating them. The example reads the score without narrowing.
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.
There was a problem hiding this comment.
2 issues found across 18 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="examples/search-and-execute.ts">
<violation number="1" location="examples/search-and-execute.ts:41">
P2: `similarity_score ?? 0` only guards null/undefined, but search hits are passed through unvalidated and can carry a non-number score. The library's own `scoreOf()` (src/toolsets.ts) and its test (`tolerating bad scores`, `similarity_score: '0.99'`) both protect against this: a string score yields `'0.99'.toFixed(3)`, which throws `TypeError: '0.99'.toFixed is not a function` and crashes the example. Keep the typeof guard (or use its number check) instead of the nullish coalescing.</violation>
</file>
<file name="src/toolsets.ts">
<violation number="1" location="src/toolsets.ts:903">
P2: This truthiness check discards an explicitly selected empty account ID, causing `submitFeedback()` to use a different configured or discovered account. Pass the selected ID whenever it is defined, or reject empty IDs before routing, so feedback cannot silently switch accounts.</violation>
</file>
Requires human review: Auto-approval skipped because cubic reviewed only this push, not the earlier force-push. Comment @cubic review to review the whole pull request.
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
|
|
||
| console.log('\nRanked matches:'); | ||
| for (const action of actions) { | ||
| console.log(` ${(action.similarity_score ?? 0).toFixed(3)} ${action.action_id}`); |
There was a problem hiding this comment.
P2: similarity_score ?? 0 only guards null/undefined, but search hits are passed through unvalidated and can carry a non-number score. The library's own scoreOf() (src/toolsets.ts) and its test (tolerating bad scores, similarity_score: '0.99') both protect against this: a string score yields '0.99'.toFixed(3), which throws TypeError: '0.99'.toFixed is not a function and crashes the example. Keep the typeof guard (or use its number check) instead of the nullish coalescing.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At examples/search-and-execute.ts, line 41:
<comment>`similarity_score ?? 0` only guards null/undefined, but search hits are passed through unvalidated and can carry a non-number score. The library's own `scoreOf()` (src/toolsets.ts) and its test (`tolerating bad scores`, `similarity_score: '0.99'`) both protect against this: a string score yields `'0.99'.toFixed(3)`, which throws `TypeError: '0.99'.toFixed is not a function` and crashes the example. Keep the typeof guard (or use its number check) instead of the nullish coalescing.</comment>
<file context>
@@ -38,8 +38,7 @@ const searchAndExecute = async (): Promise<void> => {
for (const action of actions) {
- const score = typeof action.similarity_score === 'number' ? action.similarity_score : 0;
- console.log(` ${score.toFixed(3)} ${action.action_id}`);
+ console.log(` ${(action.similarity_score ?? 0).toFixed(3)} ${action.action_id}`);
}
</file context>
| // connector where individual mode lists every action. | ||
| const [accountId] = await this.#accountsInOrder(options.accountIds); | ||
| const tool = ( | ||
| await this.fetchTools({ accountIds: accountId ? [accountId] : [], mode: 'search_execute' }) |
There was a problem hiding this comment.
P2: This truthiness check discards an explicitly selected empty account ID, causing submitFeedback() to use a different configured or discovered account. Pass the selected ID whenever it is defined, or reject empty IDs before routing, so feedback cannot silently switch accounts.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/toolsets.ts, line 903:
<comment>This truthiness check discards an explicitly selected empty account ID, causing `submitFeedback()` to use a different configured or discovered account. Pass the selected ID whenever it is defined, or reject empty IDs before routing, so feedback cannot silently switch accounts.</comment>
<file context>
@@ -860,9 +894,14 @@ export class StackOneToolSet {
+ // connector where individual mode lists every action.
+ const [accountId] = await this.#accountsInOrder(options.accountIds);
+ const tool = (
+ await this.fetchTools({ accountIds: accountId ? [accountId] : [], mode: 'search_execute' })
+ ).getTool(SUBMIT_FEEDBACK_TOOL_NAME);
if (!tool) {
</file context>
| await this.fetchTools({ accountIds: accountId ? [accountId] : [], mode: 'search_execute' }) | |
| await this.fetchTools({ accountIds: [accountId], mode: 'search_execute' }) |
Rewrites the Node SDK as the 3.0 MCP-only client, matching python#210. Graded by StackOneHQ/sdk-conformance#5. Upgrading from 2.x is covered in MIGRATION.md.
What changes
MCP-only
/mcpfor each account and executed overtools/callon the endpoint and account that listed them.GET /accountsis the only request outside MCP. When no account is configured, the SDK discovers the active accounts linked to the API key, with one request shared by concurrent callers.{isError: false, result, defenderMetadata?, policyMetadata?}for action tools, bare JSON for search.isErrorraisesStackOneAPIErrorwith the status from its payload.result, or raises with status 501 when no link can be issued.param-stylepin;Search, execute and feedback
search()ranks across every connector and puts the searchsession_idon every hit.SearchResulttypesdescription,similarity_score,input_schemaandexample_request.execute(actionId, args, { sessionId })pins the caller'saction_id, so a model-supplied one can't replace it.submitFeedback()liststool-mode=search_executeon the first account (the first ofaccountIdsif given, else the first inGET /accountsorder) and makes exactly onetools/callto the server'sstackone_submit_feedback.fetchTools()lists that tool once however many accounts serve it.Schemas and security
toJsonSchema()passes the served schema through: root keywords,$defs, andrequiredin the served order.headersobject, or top-levelheaders_<name>) are forwarded only if the served schema declares them in the same form; an openheadersobject (as on*_execute_action) declares every name.Authorization,x-account-idandUser-Agentare never forwarded, and the SDK always sets them itself, so a model can't switch tenant. Dropped headers warn with the reason.StackOneError.Also:
executeOpenAIToolCalls(), a 2.x → 3.0 migration guide, and CI that runs the conformance suite against the built package.Verification
pnpm test: 590 passed, 4 skipped.tsc --noEmiton the root project andvitest run --project rootpass withexamples/node_modulespresent and absent (as the ai-peer-range jobs install it).--strict-schema).BEGIN_COMMIT_OVERRIDE
refactor: execute every tool over MCP tools/call
BREAKING CHANGE: StackOneRpcTool, RpcExecuteConfig, BinaryDownloadResult and isBinaryDownloadResult are removed. Action results are the server's { isError, result, ... } object rather than the /actions/rpc body. A file action's
resultis the server's single-use { download_url, expires_at, file } instead of bytes, and the call raises StackOneAPIError with status 501 when no link can be issued. The /mcp URL no longer pins param-style=flat_prefixed, so tool argument names follow the server's own param style. dryRun returns { url, method: 'tools/call', name, arguments }.refactor: rebuild the toolset on the served MCP catalog
BREAKING CHANGE: RequestBuilder and direct-HTTP execution are removed: ExecuteConfig has no http kind, ParameterLocation is no longer exported, and BaseTool#execute throws unless overridden. BaseTool no longer takes headers and loses getHeaders()/setHeaders() and connector; Tools#getConnectors() is removed. StackOneTool's fifth constructor argument is the account id. StackOneToolSetConfig loses authentication, strict and rpcClient. The constructor throws ToolSetConfigError without an API key. fetchTools() returns fresh tool instances on every call, ordered by account id, and discovers accounts when none is configured.
refactor: remove AuthenticationConfig and BaseToolSetConfig
BREAKING CHANGE: AuthenticationConfig and BaseToolSetConfig are no longer exported. Pass apiKey in StackOneToolSetConfig.
refactor: remove ToolExecution#headers
BREAKING CHANGE: ToolExecution, the execution metadata toAISDK() can attach, no longer carries headers, so it can no longer expose the request credential.
refactor: remove client-side search from the Node SDK
BREAKING CHANGE: searchTools(), searchActionNames(), getSearchTool(), SearchTool, the tool_search/tool_execute meta tools, openai({ mode: 'search_and_execute' }) and the search constructor option are removed. Use the server-side search() instead.
refactor: remove SemanticSearchClient and its types
BREAKING CHANGE: SemanticSearchClient, SemanticSearchError and the SemanticSearchOptions, SemanticSearchResponse and SemanticSearchResult types are no longer exported. search() runs on the server and returns SearchResult objects.
refactor: remove the client-side search option types
BREAKING CHANGE: the SearchMode, SearchToolsOptions, SearchActionNamesOptions and SearchConfig types are no longer exported. Use SearchOptions with search().
refactor: remove getSearchConfig() and getTools()
BREAKING CHANGE: StackOneToolSet#getSearchConfig() and StackOneToolSet#getTools() are removed. For search and execute meta tools, construct the toolset with toolMode: 'search_execute' and call fetchTools().
refactor: remove SDK-side defender configuration
BREAKING CHANGE: DefenderConfig, DefenderMode and DEFAULT_DEFENDER_CONFIG are no longer exported, and the defender option and StackOneToolSet#defenderMode are removed. Configure Defender in the StackOne dashboard.
refactor: remove the client-side feedback tool
BREAKING CHANGE: createFeedbackTool is no longer exported and fetchTools() no longer appends a tool_feedback tool. Use submitFeedback() or the served stackone_submit_feedback tool.
fix(toolsets): stop model-supplied headers from switching tenant
BREAKING CHANGE: headers a tool call supplies are dropped unless the tool's served schema declares them.
fix(tools): forward only the header arguments a served schema declares
BREAKING CHANGE: a header argument (an entry of a nested headers object, or a top-level headers_ argument) is forwarded only if the served schema declares it in the same form: headers.properties., any name when headers is an object schema with no properties, or the headers_ property. Authorization, x-account-id and User-Agent are never forwarded as header arguments, even when declared. Every other argument is sent unchanged.
fix(toolsets): ignore SDK-owned names in the headers option
BREAKING CHANGE: Authorization, x-account-id and User-Agent in the headers constructor option are ignored with a warning; the SDK always sets them itself. Use apiKey and accountId/accountIds instead.
refactor: root every SDK error at StackOneError
BREAKING CHANGE: ToolSetError (and so ToolSetConfigError and ToolSetLoadError) now extends StackOneError.
fix(errors): keep the message StackOneAPIError is given
BREAKING CHANGE: StackOneAPIError no longer appends responseBody.message to its message. Read the server's explanation from error.responseBody.
feat: add search(), execute() and submitFeedback() with session ids
feat(tools): add executeOpenAIToolCalls()
feat(tool): preserve root schema keywords in toJsonSchema pass-through
END_COMMIT_OVERRIDE