Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@stackone/ai

Package Overview
Dependencies
Maintainers
3
Versions
32
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@stackone/ai - npm Package Compare versions

Comparing version
2.7.0
to
2.8.0
+99
examples/auth-management.ts
/**
* Authentication and account management patterns.
*
* Shows every way to configure API keys and account IDs with the Node SDK.
*
* Run with:
* STACKONE_API_KEY=xxx STACKONE_ACCOUNT_ID=xxx npx tsx examples/auth-management.ts
*/
import process from 'node:process';
import { StackOneToolSet } from '@stackone/ai';
if (!process.env.STACKONE_API_KEY) {
console.error('Set STACKONE_API_KEY to run this example.');
process.exit(1);
}
// --- 1. API Key setup ---
const apiKeyFromEnv = async (): Promise<void> => {
console.log('=== 1a. API Key from environment ===\n');
// Reads STACKONE_API_KEY and STACKONE_ACCOUNT_ID from env automatically
const toolset = new StackOneToolSet();
const tools = await toolset.fetchTools();
console.log(` Loaded ${tools.toOpenAI().length} tools using env API key\n`);
};
const apiKeyExplicit = async (): Promise<void> => {
console.log('=== 1b. Explicit API key ===\n');
const toolset = new StackOneToolSet({ apiKey: process.env.STACKONE_API_KEY });
const tools = await toolset.fetchTools();
console.log(` Loaded ${tools.toOpenAI().length} tools using explicit API key\n`);
};
// --- 2. Account ID from environment ---
const accountIdFromEnv = async (): Promise<void> => {
console.log('=== 2. Account ID from environment ===\n');
// The Node SDK reads STACKONE_ACCOUNT_ID from env automatically
const accountId = process.env.STACKONE_ACCOUNT_ID;
console.log(` STACKONE_ACCOUNT_ID is ${accountId ? 'set' : 'not set'}`);
const toolset = new StackOneToolSet();
const tools = await toolset.fetchTools();
console.log(` Loaded ${tools.toOpenAI().length} tools for account from env\n`);
};
// --- 3. Account ID in constructor ---
const accountIdInConstructor = async (): Promise<void> => {
console.log('=== 3. Account ID in constructor ===\n');
const accountId = process.env.STACKONE_ACCOUNT_ID ?? 'my-account';
const toolset = new StackOneToolSet({ accountId });
const tools = await toolset.fetchTools();
console.log(` Loaded ${tools.toOpenAI().length} tools for configured account\n`);
};
// --- 4. setAccounts() — set accounts globally ---
const setAccountsGlobally = async (): Promise<void> => {
console.log('=== 4. setAccounts() — global account list ===\n');
const accountId = process.env.STACKONE_ACCOUNT_ID ?? 'my-account';
const toolset = new StackOneToolSet();
toolset.setAccounts([accountId]);
console.log(' Called setAccounts() with configured account');
// Subsequent fetchTools uses the globally set accounts
const tools = await toolset.fetchTools();
console.log(` Loaded ${tools.toOpenAI().length} tools after setAccounts()\n`);
};
// --- 5. Per-tool account override ---
const perToolOverride = async (): Promise<void> => {
console.log('=== 5. Per-tool account override ===\n');
const toolset = new StackOneToolSet();
const tools = await toolset.fetchTools();
// Override account on a single tool (use getStackOneTool for account methods)
try {
const tool = tools.getStackOneTool('workday_list_workers');
tool.setAccountId('per-tool-account');
const current = tool.getAccountId();
console.log(` Single tool account: "${current}"\n`);
} catch {
console.log(' (workday_list_workers not available — skipping single-tool demo)\n');
}
};
// --- Run all sections ---
await apiKeyFromEnv();
await apiKeyExplicit();
await accountIdFromEnv();
await accountIdInConstructor();
await setAccountsGlobally();
await perToolOverride();
console.log('Done — all auth patterns demonstrated.');
+1
-1
//#region package.json
var version = "2.7.0";
var version = "2.8.0";
var peerDependencies = {

@@ -4,0 +4,0 @@ "@anthropic-ai/claude-agent-sdk": "catalog:peer",

@@ -1,1 +0,1 @@

{"version":3,"file":"package.mjs","names":[],"sources":["../package.json"],"sourcesContent":["{\n\t\"name\": \"@stackone/ai\",\n\t\"version\": \"2.7.0\",\n\t\"description\": \"Tools for agents to perform actions on your SaaS\",\n\t\"keywords\": [\n\t\t\"agents\",\n\t\t\"ai\",\n\t\t\"ai sdk\",\n\t\t\"mcp\",\n\t\t\"model context protocol\",\n\t\t\"stackone\",\n\t\t\"tools\"\n\t],\n\t\"homepage\": \"https://github.com/StackOneHQ/stackone-ai-node#readme\",\n\t\"bugs\": \"https://github.com/StackOneHQ/stackone-ai-node/issues\",\n\t\"license\": \"Apache-2.0\",\n\t\"author\": \"StackOne\",\n\t\"repository\": {\n\t\t\"type\": \"git\",\n\t\t\"url\": \"git+https://github.com/StackOneHQ/stackone-ai-node.git\"\n\t},\n\t\"files\": [\n\t\t\"dist\",\n\t\t\"LICENSE\",\n\t\t\"README.md\",\n\t\t\"src\",\n\t\t\"!examples/*.test.ts\",\n\t\t\"examples/*.ts\",\n\t\t\"!src/**/*.test-d.ts\",\n\t\t\"!src/**/*.test.ts\"\n\t],\n\t\"type\": \"module\",\n\t\"main\": \"./dist/index.mjs\",\n\t\"module\": \"./dist/index.mjs\",\n\t\"types\": \"./dist/index.d.mts\",\n\t\"exports\": {\n\t\t\".\": \"./src/index.ts\",\n\t\t\"./package.json\": \"./package.json\"\n\t},\n\t\"publishConfig\": {\n\t\t\"exports\": {\n\t\t\t\".\": \"./dist/index.mjs\",\n\t\t\t\"./package.json\": \"./package.json\"\n\t\t}\n\t},\n\t\"scripts\": {\n\t\t\"build\": \"tsdown\",\n\t\t\"format\": \"pnpm --no-bail --aggregate-output run '/^format:/'\",\n\t\t\"format:oxfmt\": \"oxfmt --no-error-on-unmatched-pattern .\",\n\t\t\"format:oxlint\": \"oxlint --max-warnings=0 --type-aware --type-check --fix\",\n\t\t\"format:knip\": \"knip --fix --no-exit-code\",\n\t\t\"lint\": \"pnpm --aggregate-output run '/^lint:/'\",\n\t\t\"lint:oxfmt\": \"oxfmt --no-error-on-unmatched-pattern --check .\",\n\t\t\"lint:oxlint\": \"oxlint --max-warnings=0 --type-aware --type-check\",\n\t\t\"lint:knip\": \"knip\",\n\t\t\"prepack\": \"npm pkg delete scripts.preinstall && pnpm run build\",\n\t\t\"test\": \"vitest\",\n\t\t\"coverage\": \"vitest run --coverage\"\n\t},\n\t\"dependencies\": {\n\t\t\"@modelcontextprotocol/sdk\": \"catalog:prod\",\n\t\t\"@orama/orama\": \"catalog:prod\",\n\t\t\"defu\": \"catalog:prod\"\n\t},\n\t\"devDependencies\": {\n\t\t\"@fast-check/vitest\": \"catalog:dev\",\n\t\t\"@hono/mcp\": \"catalog:dev\",\n\t\t\"@types/node\": \"catalog:dev\",\n\t\t\"@vitest/coverage-v8\": \"catalog:dev\",\n\t\t\"ai\": \"catalog:dev\",\n\t\t\"hono\": \"catalog:dev\",\n\t\t\"knip\": \"catalog:dev\",\n\t\t\"msw\": \"catalog:dev\",\n\t\t\"openai\": \"catalog:peer\",\n\t\t\"publint\": \"catalog:dev\",\n\t\t\"tsdown\": \"catalog:dev\",\n\t\t\"type-fest\": \"catalog:dev\",\n\t\t\"unplugin-unused\": \"catalog:dev\",\n\t\t\"vitest\": \"catalog:dev\",\n\t\t\"zod\": \"catalog:dev\"\n\t},\n\t\"peerDependencies\": {\n\t\t\"@anthropic-ai/claude-agent-sdk\": \"catalog:peer\",\n\t\t\"@anthropic-ai/sdk\": \"catalog:peer\",\n\t\t\"ai\": \"catalog:peer\",\n\t\t\"openai\": \"catalog:peer\",\n\t\t\"zod\": \"catalog:peer\"\n\t},\n\t\"peerDependenciesMeta\": {\n\t\t\"@anthropic-ai/claude-agent-sdk\": {\n\t\t\t\"optional\": true\n\t\t},\n\t\t\"@anthropic-ai/sdk\": {\n\t\t\t\"optional\": true\n\t\t},\n\t\t\"ai\": {\n\t\t\t\"optional\": true\n\t\t},\n\t\t\"openai\": {\n\t\t\t\"optional\": true\n\t\t}\n\t},\n\t\"devEngines\": {\n\t\t\"runtime\": [\n\t\t\t{\n\t\t\t\t\"name\": \"node\",\n\t\t\t\t\"version\": \"^24.11.0\",\n\t\t\t\t\"onFail\": \"download\"\n\t\t\t}\n\t\t]\n\t},\n\t\"engines\": {\n\t\t\"node\": \">=20.19.6\"\n\t},\n\t\"packageManager\": \"pnpm@10.26.0\"\n}\n"],"mappings":";cAEY;uBA+ES;CACnB,kCAAkC;CAClC,qBAAqB;CACrB,MAAM;CACN,UAAU;CACV,OAAO;CACP"}
{"version":3,"file":"package.mjs","names":[],"sources":["../package.json"],"sourcesContent":["{\n\t\"name\": \"@stackone/ai\",\n\t\"version\": \"2.8.0\",\n\t\"description\": \"Tools for agents to perform actions on your SaaS\",\n\t\"keywords\": [\n\t\t\"agents\",\n\t\t\"ai\",\n\t\t\"ai sdk\",\n\t\t\"mcp\",\n\t\t\"model context protocol\",\n\t\t\"stackone\",\n\t\t\"tools\"\n\t],\n\t\"homepage\": \"https://github.com/StackOneHQ/stackone-ai-node#readme\",\n\t\"bugs\": \"https://github.com/StackOneHQ/stackone-ai-node/issues\",\n\t\"license\": \"Apache-2.0\",\n\t\"author\": \"StackOne\",\n\t\"repository\": {\n\t\t\"type\": \"git\",\n\t\t\"url\": \"git+https://github.com/StackOneHQ/stackone-ai-node.git\"\n\t},\n\t\"files\": [\n\t\t\"dist\",\n\t\t\"LICENSE\",\n\t\t\"README.md\",\n\t\t\"src\",\n\t\t\"examples/*.ts\",\n\t\t\"!src/**/*.test-d.ts\",\n\t\t\"!src/**/*.test.ts\"\n\t],\n\t\"type\": \"module\",\n\t\"main\": \"./dist/index.mjs\",\n\t\"module\": \"./dist/index.mjs\",\n\t\"types\": \"./dist/index.d.mts\",\n\t\"exports\": {\n\t\t\".\": \"./src/index.ts\",\n\t\t\"./package.json\": \"./package.json\"\n\t},\n\t\"publishConfig\": {\n\t\t\"exports\": {\n\t\t\t\".\": \"./dist/index.mjs\",\n\t\t\t\"./package.json\": \"./package.json\"\n\t\t}\n\t},\n\t\"scripts\": {\n\t\t\"build\": \"tsdown\",\n\t\t\"format\": \"pnpm --no-bail --aggregate-output run '/^format:/'\",\n\t\t\"format:oxfmt\": \"oxfmt --no-error-on-unmatched-pattern .\",\n\t\t\"format:oxlint\": \"oxlint --max-warnings=0 --type-aware --type-check --fix\",\n\t\t\"format:knip\": \"knip --fix --no-exit-code\",\n\t\t\"lint\": \"pnpm --aggregate-output run '/^lint:/'\",\n\t\t\"lint:oxfmt\": \"oxfmt --no-error-on-unmatched-pattern --check .\",\n\t\t\"lint:oxlint\": \"oxlint --max-warnings=0 --type-aware --type-check\",\n\t\t\"lint:knip\": \"knip\",\n\t\t\"prepack\": \"npm pkg delete scripts.preinstall && pnpm run build\",\n\t\t\"test\": \"vitest\",\n\t\t\"coverage\": \"vitest run --coverage\",\n\t\t\"run:example\": \"tsx --env-file .env\"\n\t},\n\t\"dependencies\": {\n\t\t\"@modelcontextprotocol/sdk\": \"catalog:prod\",\n\t\t\"@orama/orama\": \"catalog:prod\",\n\t\t\"defu\": \"catalog:prod\"\n\t},\n\t\"devDependencies\": {\n\t\t\"@fast-check/vitest\": \"catalog:dev\",\n\t\t\"@hono/mcp\": \"catalog:dev\",\n\t\t\"@types/node\": \"catalog:dev\",\n\t\t\"@vitest/coverage-v8\": \"catalog:dev\",\n\t\t\"ai\": \"catalog:dev\",\n\t\t\"hono\": \"catalog:dev\",\n\t\t\"knip\": \"catalog:dev\",\n\t\t\"msw\": \"catalog:dev\",\n\t\t\"openai\": \"catalog:peer\",\n\t\t\"publint\": \"catalog:dev\",\n\t\t\"tsdown\": \"catalog:dev\",\n\t\t\"type-fest\": \"catalog:dev\",\n\t\t\"unplugin-unused\": \"catalog:dev\",\n\t\t\"vitest\": \"catalog:dev\",\n\t\t\"zod\": \"catalog:dev\"\n\t},\n\t\"peerDependencies\": {\n\t\t\"@anthropic-ai/claude-agent-sdk\": \"catalog:peer\",\n\t\t\"@anthropic-ai/sdk\": \"catalog:peer\",\n\t\t\"ai\": \"catalog:peer\",\n\t\t\"openai\": \"catalog:peer\",\n\t\t\"zod\": \"catalog:peer\"\n\t},\n\t\"peerDependenciesMeta\": {\n\t\t\"@anthropic-ai/claude-agent-sdk\": {\n\t\t\t\"optional\": true\n\t\t},\n\t\t\"@anthropic-ai/sdk\": {\n\t\t\t\"optional\": true\n\t\t},\n\t\t\"ai\": {\n\t\t\t\"optional\": true\n\t\t},\n\t\t\"openai\": {\n\t\t\t\"optional\": true\n\t\t}\n\t},\n\t\"devEngines\": {\n\t\t\"runtime\": [\n\t\t\t{\n\t\t\t\t\"name\": \"node\",\n\t\t\t\t\"version\": \"^24.11.0\",\n\t\t\t\t\"onFail\": \"download\"\n\t\t\t}\n\t\t]\n\t},\n\t\"engines\": {\n\t\t\"node\": \">=20.19.6\"\n\t},\n\t\"packageManager\": \"pnpm@10.26.0\"\n}\n"],"mappings":";cAEY;uBA+ES;CACnB,kCAAkC;CAClC,qBAAqB;CACrB,MAAM;CACN,UAAU;CACV,OAAO;CACP"}

@@ -13,3 +13,2 @@ /**

import assert from 'node:assert';
import process from 'node:process';

@@ -25,2 +24,6 @@ import { openai } from '@ai-sdk/openai';

}
if (!process.env.OPENAI_API_KEY) {
console.log('Skipping: OPENAI_API_KEY is not set');
process.exit(0);
}

@@ -31,19 +34,32 @@ const aiSdkIntegration = async (): Promise<void> => {

// Fetch all tools for this account via MCP
const tools = await toolset.fetchTools();
// Filter to specific tools to keep token usage manageable
const tools = await toolset.fetchTools({
actions: ['workday_list_workers', 'workday_get_worker', 'workday_get_current_user'],
});
// Convert to AI SDK tools
const aiSdkTools = await tools.toAISDK();
console.log(`Loaded ${Object.keys(aiSdkTools).length} tools for AI SDK`);
// The AI SDK will automatically call the tool if needed
const { text } = await generateText({
const { text, steps } = await generateText({
model: openai('gpt-5.1'),
tools: aiSdkTools,
prompt: 'Get all details about employee with id: c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA',
prompt: 'List the first 5 employees',
stopWhen: stepCountIs(3),
});
assert(text.includes('Michael'), 'Expected employee name to be included in the response');
console.log(`AI response: ${text}`);
console.log(`Steps taken: ${steps.length}`);
for (const step of steps) {
if (step.toolCalls && step.toolCalls.length > 0) {
for (const toolCall of step.toolCalls) {
console.log(` Tool call: ${toolCall.toolName}`);
console.log(` Arguments: ${JSON.stringify((toolCall as Record<string, unknown>).args)}`);
}
}
}
};
await aiSdkIntegration();

@@ -5,3 +5,2 @@ /**

import assert from 'node:assert';
import process from 'node:process';

@@ -16,2 +15,6 @@ import Anthropic from '@anthropic-ai/sdk';

}
if (!process.env.ANTHROPIC_API_KEY) {
console.log('Skipping: ANTHROPIC_API_KEY is not set');
process.exit(0);
}

@@ -27,2 +30,3 @@ const anthropicIntegration = async (): Promise<void> => {

const anthropicTools = tools.toAnthropic();
console.log(`Loaded ${anthropicTools.length} tools for Anthropic`);

@@ -40,3 +44,3 @@ // Initialize Anthropic client

role: 'user',
content: 'What is the employee with id: c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA phone number?',
content: 'List the first 5 employees',
},

@@ -47,13 +51,12 @@ ],

// Verify the response contains tool use
assert(response.content.length > 0, 'Expected at least one content block in the response');
console.log(`Response content blocks: ${response.content.length}`);
const toolUseBlock = response.content.find((block) => block.type === 'tool_use');
assert(toolUseBlock !== undefined, 'Expected a tool_use block in the response');
assert(toolUseBlock.type === 'tool_use', 'Expected block to be tool_use type');
assert(toolUseBlock.name === 'hris_get_employee', 'Expected tool call to be hris_get_employee');
// Verify the input contains the expected fields
const input = toolUseBlock.input as Record<string, unknown>;
assert(input.id === 'c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA', 'Expected id to match the query');
for (const block of response.content) {
if (block.type === 'tool_use') {
console.log(` Tool use: ${block.name}`);
console.log(` Input: ${JSON.stringify(block.input)}`);
} else if (block.type === 'text') {
console.log(` Text: ${block.text}`);
}
}
};

@@ -60,0 +63,0 @@

@@ -11,3 +11,2 @@ /**

import assert from 'node:assert';
import process from 'node:process';

@@ -22,2 +21,6 @@ import { query } from '@anthropic-ai/claude-agent-sdk';

}
if (!process.env.ANTHROPIC_API_KEY) {
console.log('Skipping: ANTHROPIC_API_KEY is not set');
process.exit(0);
}

@@ -31,2 +34,3 @@ const claudeAgentSdkIntegration = async (): Promise<void> => {

const mcpServer = await tools.toClaudeAgentSdk();
console.log('Claude Agent SDK MCP server created');

@@ -36,3 +40,3 @@ // Use the Claude Agent SDK query with the StackOne MCP server

const result = query({
prompt: 'Get the employee with id: c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA',
prompt: 'List the first 5 employees',
options: {

@@ -51,8 +55,11 @@ model: 'claude-sonnet-4-5-20250929',

// Process the stream and collect results
let hasToolCall = false;
console.log('Processing agent stream...');
for await (const message of result) {
if (message.type === 'assistant') {
for (const block of message.message.content) {
if (block.type === 'tool_use' && block.name === 'hris_get_employee') {
hasToolCall = true;
if (block.type === 'tool_use') {
console.log(` Tool use: ${block.name}`);
console.log(` Input: ${JSON.stringify(block.input)}`);
} else if (block.type === 'text') {
console.log(` Assistant: ${block.text}`);
}

@@ -63,5 +70,5 @@ }

assert(hasToolCall, 'Expected at least one tool call to hris_get_employee');
console.log('Agent stream completed');
};
await claudeAgentSdkIntegration();

@@ -5,3 +5,2 @@ /**

import assert from 'node:assert';
import process from 'node:process';

@@ -16,2 +15,6 @@ import { StackOneToolSet } from '@stackone/ai';

}
if (!process.env.OPENAI_API_KEY) {
console.log('Skipping: OPENAI_API_KEY is not set');
process.exit(0);
}

@@ -22,5 +25,8 @@ const openaiIntegration = async (): Promise<void> => {

// Fetch all tools for this account via MCP
const tools = await toolset.fetchTools();
// Filter to specific tools to stay within OpenAI's 128-tool limit
const tools = await toolset.fetchTools({
actions: ['workday_list_workers', 'workday_get_worker', 'workday_get_current_user'],
});
const openAITools = tools.toOpenAI();
console.log(`Loaded ${openAITools.length} tools for OpenAI`);

@@ -36,7 +42,7 @@ // Initialize OpenAI client

role: 'system',
content: 'You are a helpful assistant that can access BambooHR information.',
content: 'You are a helpful assistant that can access HR information.',
},
{
role: 'user',
content: 'What is the employee with id: c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA phone number?',
content: 'List the first 5 employees',
},

@@ -47,19 +53,14 @@ ],

// Verify the response contains tool calls
assert(response.choices.length > 0, 'Expected at least one choice in the response');
console.log(`Model returned ${response.choices.length} choice(s)`);
const choice = response.choices[0];
assert(choice.message.tool_calls !== undefined, 'Expected tool_calls to be defined');
assert(choice.message.tool_calls.length > 0, 'Expected at least one tool call');
const toolCalls = choice.message.tool_calls ?? [];
console.log(`Tool calls made: ${toolCalls.length}`);
const toolCall = choice.message.tool_calls[0];
assert(toolCall.type === 'function', 'Expected tool call to be a function');
assert(
toolCall.function.name === 'bamboohr_get_employee',
'Expected tool call to be bamboohr_get_employee',
);
// Parse the arguments to verify they contain the expected fields
const args = JSON.parse(toolCall.function.arguments);
assert(args.id === 'c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA', 'Expected id to match the query');
for (const toolCall of toolCalls) {
if ('function' in toolCall) {
console.log(` Tool: ${toolCall.function.name}`);
console.log(` Arguments: ${toolCall.function.arguments}`);
}
}
};

@@ -66,0 +67,0 @@

@@ -5,3 +5,2 @@ /**

import assert from 'node:assert';
import process from 'node:process';

@@ -16,2 +15,6 @@ import { StackOneToolSet } from '@stackone/ai';

}
if (!process.env.OPENAI_API_KEY) {
console.log('Skipping: OPENAI_API_KEY is not set');
process.exit(0);
}

@@ -27,2 +30,3 @@ const openaiResponsesIntegration = async (): Promise<void> => {

const openAIResponsesTools = tools.toOpenAIResponses();
console.log(`Loaded ${openAIResponsesTools.length} tools for OpenAI Responses API`);

@@ -36,9 +40,8 @@ // Initialize OpenAI client

instructions: 'You are a helpful assistant that can access various tools.',
input: 'What is the employee with id: c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA phone number?',
input: 'List the first 5 employees',
tools: openAIResponsesTools,
});
// Verify the response contains expected data
assert(response.id, 'Expected response to have an ID');
assert(response.model, 'Expected response to have a model');
console.log(`Response ID: ${response.id}`);
console.log(`Model: ${response.model}`);

@@ -50,13 +53,8 @@ // Check if the model made any tool calls

assert(toolCalls.length > 0, 'Expected at least one tool call');
console.log(`Tool calls found: ${toolCalls.length}`);
const toolCall = toolCalls[0];
assert(
toolCall.name === 'bamboohr_get_employee',
'Expected tool call to be bamboohr_get_employee',
);
// Parse the arguments to verify they contain the expected fields
const args = JSON.parse(toolCall.arguments);
assert(args.id === 'c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA', 'Expected id to match the query');
for (const toolCall of toolCalls) {
console.log(` Tool: ${toolCall.name}`);
console.log(` Arguments: ${toolCall.arguments}`);
}
};

@@ -63,0 +61,0 @@

/**
* Search tool patterns: callable wrapper and config overrides.
* Tool discovery with the StackOne AI SDK.
*
* For full agent execution, see agent-tool-search.ts.
* Covers: direct fetch, semantic search, local search, auto search,
* and the search-and-execute pattern.
*

@@ -25,26 +26,152 @@ * Run with:

// --- Example 1: getSearchTool() callable ---
console.log('=== getSearchTool() callable ===\n');
// ---------------------------------------------------------------------------
// 1. Direct fetch — no search, just action-pattern filtering
// ---------------------------------------------------------------------------
async function directFetch(): Promise<void> {
console.log('\n=== 1. Direct Fetch (action filters) ===\n');
const toolset = new StackOneToolSet({ apiKey, accountId, search: {} });
const searchTool = toolset.getSearchTool();
const toolset = new StackOneToolSet({ apiKey, accountId });
const queries = ['cancel an event', 'list employees', 'send a message'];
for (const query of queries) {
const tools = await searchTool.search(query, { topK: 3 });
// Glob pattern: fetch only Workday tools
const tools = await toolset.fetchTools({ actions: ['workday_*'] });
const names = tools.toArray().map((t) => t.name);
console.log(` "${query}" -> ${names.join(', ') || '(none)'}`);
console.log(` Fetched ${names.length} tools matching "workday_*"`);
console.log(` First 5: ${names.slice(0, 5).join(', ')}`);
}
// --- Example 2: Constructor topK vs per-call override ---
console.log('\n=== Constructor topK vs per-call override ===\n');
// ---------------------------------------------------------------------------
// 2. Semantic search — remote embedding-based similarity
// ---------------------------------------------------------------------------
async function semanticSearch(): Promise<void> {
console.log('\n=== 2. Semantic Search ===\n');
const toolset3 = new StackOneToolSet({ apiKey, accountId, search: { topK: 3 } });
const toolset = new StackOneToolSet({
apiKey,
accountId,
search: { method: 'semantic', topK: 5 },
});
const query = 'manage employee records';
const tools = await toolset.searchTools('manage employees');
const names = tools.toArray().map((t) => t.name);
console.log(` Query: "manage employees" -> ${names.length} results`);
for (const name of names) {
console.log(` - ${name}`);
}
}
const tools3 = await toolset3.searchTools(query);
console.log(`Constructor topK=3: got ${tools3.length} tools`);
// ---------------------------------------------------------------------------
// 3. Local search — BM25 + TF-IDF, no API call to semantic endpoint
// ---------------------------------------------------------------------------
async function localSearch(): Promise<void> {
console.log('\n=== 3. Local Search (BM25 + TF-IDF) ===\n');
const toolsOverride = await toolset3.searchTools(query, { topK: 10 });
console.log(`Per-call topK=10 (overrides constructor 3): got ${toolsOverride.length} tools`);
const toolset = new StackOneToolSet({
apiKey,
accountId,
search: { method: 'local', topK: 5 },
});
const tools = await toolset.searchTools('time off requests', { search: 'local' });
const names = tools.toArray().map((t) => t.name);
console.log(` Query: "time off requests" -> ${names.length} results`);
for (const name of names) {
console.log(` - ${name}`);
}
}
// ---------------------------------------------------------------------------
// 4. Auto search + getSearchTool() callable
// Tries semantic first, falls back to local if the API is unavailable.
// ---------------------------------------------------------------------------
async function autoSearchWithCallable(): Promise<void> {
console.log('\n=== 4. Auto Search + getSearchTool() Callable ===\n');
const toolset = new StackOneToolSet({
apiKey,
accountId,
search: { method: 'auto', topK: 5 },
});
// 4a. Direct searchTools() with auto mode
const tools = await toolset.searchTools('send a message', { search: 'auto' });
console.log(` searchTools("send a message") -> ${tools.length} results`);
// 4b. getSearchTool() — a callable wrapper for agent loops
const searchTool = toolset.getSearchTool();
const queries = ['cancel an event', 'list employees', 'create a job posting'];
for (const query of queries) {
const results = await searchTool.search(query, { topK: 3 });
const names = results.toArray().map((t) => t.name);
console.log(` searchTool.search("${query}") -> ${names.join(', ') || '(none)'}`);
}
// 4c. Constructor topK vs per-call override
console.log('\n -- topK override --');
const defaultResults = await toolset.searchTools('manage employee records');
console.log(` Constructor topK=5: got ${defaultResults.length} tools`);
const overrideResults = await toolset.searchTools('manage employee records', { topK: 10 });
console.log(` Per-call topK=10: got ${overrideResults.length} tools`);
}
// ---------------------------------------------------------------------------
// 5. Search & execute mode — getTools() with Vercel AI SDK
// The LLM receives tool_search + tool_execute and discovers tools on demand.
// ---------------------------------------------------------------------------
async function searchAndExecute(): Promise<void> {
console.log('\n=== 5. Search & Execute (Vercel AI SDK) ===\n');
// Dynamic imports — these are optional peer dependencies
const [{ openai }, { generateText, stepCountIs }] = await Promise.all([
import('@ai-sdk/openai'),
import('ai'),
]);
const toolset = new StackOneToolSet({
apiKey,
accountId,
search: { method: 'semantic', topK: 3 },
timeout: 120_000, // increase for slow providers (default: 60s)
});
// getTools() returns tool_search + tool_execute as a Tools collection
const tools = toolset.getTools({ accountIds: [accountId as string] });
console.log(' Tools provided to model: tool_search, tool_execute');
const { text, steps } = await generateText({
model: openai('gpt-5.1'),
tools: await tools.toAISDK(),
prompt: 'List employees and return a short summary.',
stopWhen: stepCountIs(5),
});
console.log(` Model completed in ${steps.length} step(s)`);
console.log(` Response: ${text.slice(0, 200)}${text.length > 200 ? '...' : ''}`);
}
// ---------------------------------------------------------------------------
// Main — run each section in order
// ---------------------------------------------------------------------------
async function main(): Promise<void> {
await directFetch();
await semanticSearch();
await localSearch();
await autoSearchWithCallable();
// Section 5 requires @ai-sdk/openai and ai packages + an OPENAI_API_KEY.
// Skip gracefully if not available.
try {
await searchAndExecute();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (message.includes('Cannot find module') || message.includes('Cannot find package')) {
console.log('\n=== 5. Search & Execute (Vercel AI SDK) ===\n');
console.log(' Skipped: install @ai-sdk/openai and ai to run this section.');
} else {
throw error;
}
}
}
await main();
{
"name": "@stackone/ai",
"version": "2.7.0",
"version": "2.8.0",
"description": "Tools for agents to perform actions on your SaaS",

@@ -27,3 +27,2 @@ "keywords": [

"src",
"!examples/*.test.ts",
"examples/*.ts",

@@ -108,4 +107,5 @@ "!src/**/*.test-d.ts",

"test": "vitest",
"coverage": "vitest run --coverage"
"coverage": "vitest run --coverage",
"run:example": "tsx --env-file .env"
}
}
+53
-87

@@ -36,8 +36,7 @@ # StackOne AI SDK

const toolset = new StackOneToolSet({
accountId: 'your-account-id',
});
// Reads STACKONE_API_KEY and STACKONE_ACCOUNT_ID from environment
const toolset = new StackOneToolSet();
const tools = await toolset.fetchTools();
const employeeTool = tools.getTool('bamboohr_list_employees');
const employeeTool = tools.getTool('workday_list_workers');
const employees = await employeeTool.execute();

@@ -63,16 +62,20 @@ ```

// Single account - simplest approach
const toolset = new StackOneToolSet({ accountId: 'your-bamboohr-account' });
// Simplest: set STACKONE_ACCOUNT_ID environment variable
const toolset = new StackOneToolSet();
const tools = await toolset.fetchTools();
// Explicit single account
const explicitToolset = new StackOneToolSet({ accountId: 'your-workday-account' });
const explicitTools = await explicitToolset.fetchTools();
// Multiple accounts - returns tools from both integrations
const multiAccountToolset = new StackOneToolSet();
const allTools = await multiAccountToolset.fetchTools({
accountIds: ['bamboohr-account-123', 'workday-account-456'],
accountIds: ['workday-account-123', 'hibob-account-456'],
});
// Filter to specific integration when using multiple accounts
const bamboohrOnly = await multiAccountToolset.fetchTools({
accountIds: ['bamboohr-account-123', 'workday-account-456'],
actions: ['bamboohr_*'], // Only BambooHR tools
const workdayOnly = await multiAccountToolset.fetchTools({
accountIds: ['workday-account-123', 'hibob-account-456'],
actions: ['workday_*'], // Only Workday tools
});

@@ -100,5 +103,4 @@

const toolset = new StackOneToolSet({
accountId: 'your-account-id',
});
// Reads STACKONE_API_KEY and STACKONE_ACCOUNT_ID from environment
const toolset = new StackOneToolSet();

@@ -112,3 +114,3 @@ const tools = await toolset.fetchTools();

role: 'system',
content: 'You are a helpful HR assistant using BambooHR.',
content: 'You are a helpful HR assistant using Workday.',
},

@@ -139,5 +141,4 @@ {

const toolset = new StackOneToolSet({
accountId: 'your-account-id',
});
// Reads STACKONE_API_KEY and STACKONE_ACCOUNT_ID from environment
const toolset = new StackOneToolSet();

@@ -171,5 +172,4 @@ const tools = await toolset.fetchTools();

const toolset = new StackOneToolSet({
accountId: 'your-account-id',
});
// Reads STACKONE_API_KEY and STACKONE_ACCOUNT_ID from environment
const toolset = new StackOneToolSet();

@@ -210,5 +210,4 @@ const tools = await toolset.fetchTools();

const toolset = new StackOneToolSet({
accountId: 'your-account-id',
});
// Reads STACKONE_API_KEY and STACKONE_ACCOUNT_ID from environment
const toolset = new StackOneToolSet();

@@ -229,51 +228,2 @@ const tools = await toolset.fetchTools();

<details>
<summary><strong>With TanStack AI</strong></summary>
```bash
npm install @stackone/ai @tanstack/ai @tanstack/ai-openai zod # or: yarn/pnpm/bun add
```
```typescript
import { chat } from '@tanstack/ai';
import { openai } from '@tanstack/ai-openai';
import { z } from 'zod';
import { StackOneToolSet } from '@stackone/ai';
const toolset = new StackOneToolSet({
accountId: 'your-account-id',
});
const tools = await toolset.fetchTools();
const employeeTool = tools.getTool('bamboohr_get_employee');
// TanStack AI requires Zod schemas for tool input validation
const getEmployeeTool = {
name: employeeTool.name,
description: employeeTool.description,
inputSchema: z.object({
id: z.string().describe('The employee ID'),
}),
execute: async (args: { id: string }) => {
return employeeTool.execute(args);
},
};
const adapter = openai();
const stream = chat({
adapter,
model: 'gpt-5.1',
messages: [{ role: 'user', content: 'Get employee with id: abc123' }],
tools: [getEmployeeTool],
});
for await (const chunk of stream) {
// Process streaming chunks
}
```
[View full example](examples/tanstack-ai-integration.ts)
</details>
<details>
<summary><strong>With Claude Agent SDK</strong></summary>

@@ -289,5 +239,4 @@

const toolset = new StackOneToolSet({
accountId: 'your-account-id',
});
// Reads STACKONE_API_KEY and STACKONE_ACCOUNT_ID from environment
const toolset = new StackOneToolSet();

@@ -334,3 +283,3 @@ // Fetch tools and convert to Claude Agent SDK format

// Filter by providers
const tools = await toolset.fetchTools({ providers: ['hibob', 'bamboohr'] });
const tools = await toolset.fetchTools({ providers: ['hibob', 'workday'] });

@@ -359,4 +308,2 @@ // Filter by actions with exact match

[View full example](examples/fetch-tools.ts)
### Search Tool

@@ -371,4 +318,4 @@

// Get a callable search tool
const toolset = new StackOneToolSet({ accountId: 'your-account-id' });
// Reads STACKONE_API_KEY and STACKONE_ACCOUNT_ID from environment
const toolset = new StackOneToolSet();
const searchTool = toolset.getSearchTool();

@@ -380,3 +327,3 @@

// Execute a discovered tool directly
const listTool = tools.getTool('bamboohr_list_employees');
const listTool = tools.getTool('workday_list_workers');
const result = await listTool.execute({ query: { limit: 10 } });

@@ -387,3 +334,3 @@ ```

Discover tools using natural language instead of exact names. Queries like "onboard new hire" resolve to the right actions even when the tool is called `bamboohr_create_employee`.
Discover tools using natural language instead of exact names. Queries like "onboard new hire" resolve to the right actions even when the tool is called `workday_create_employee`.

@@ -393,3 +340,4 @@ ```typescript

const toolset = new StackOneToolSet({ accountId: 'your-account-id' });
// Reads STACKONE_API_KEY and STACKONE_ACCOUNT_ID from environment
const toolset = new StackOneToolSet();

@@ -440,3 +388,3 @@ // Search by intent — returns Tools collection ready for any framework

const tools = await toolset.fetchTools();
const employeeTool = tools.getTool('bamboohr_list_employees');
const employeeTool = tools.getTool('workday_list_workers');

@@ -509,3 +457,3 @@ // Use dryRun to see the request details

account_id: 'acc_123456',
tool_names: ['bamboohr_list_employees', 'bamboohr_create_time_off'],
tool_names: ['workday_list_workers', 'workday_create_time_off_request'],
});

@@ -523,3 +471,3 @@ ```

account_id: 'acc_123456',
tool_names: ['bamboohr_list_employees', 'bamboohr_create_time_off'],
tool_names: ['workday_list_workers', 'workday_create_time_off_request'],
});

@@ -531,3 +479,3 @@

account_id: ['acc_123456', 'acc_789012'],
tool_names: ['bamboohr_list_employees', 'bamboohr_create_time_off'],
tool_names: ['workday_list_workers', 'workday_create_time_off_request'],
});

@@ -571,2 +519,20 @@ ```

## Examples
### Running Examples
```bash
# 1. Set up credentials
cp .env.example .env
# Edit .env with your API keys
# 2. Install dependencies
pnpm install
# 3. Run any example
pnpm run:example examples/openai-integration.ts
```
See the [examples/](examples/) directory for the full list.
## Development Environment

@@ -573,0 +539,0 @@

/**
* This example demonstrates the search and execute tools pattern (tool_search + tool_execute)
* for LLM-driven tool discovery and execution.
*
* Instead of loading all tools upfront, the LLM autonomously searches for
* relevant tools and executes them — keeping token usage minimal.
*
* @example
* ```bash
* # Run with required environment variables:
* STACKONE_API_KEY=your-key OPENAI_API_KEY=your-key STACKONE_ACCOUNT_ID=your-account npx tsx examples/agent-tool-search.ts
* ```
*/
import process from 'node:process';
import { openai } from '@ai-sdk/openai';
import { StackOneToolSet } from '@stackone/ai';
import { generateText, stepCountIs } from 'ai';
import OpenAI from 'openai';
const apiKey = process.env.STACKONE_API_KEY;
if (!apiKey) {
console.error('STACKONE_API_KEY environment variable is required');
process.exit(1);
}
if (!process.env.OPENAI_API_KEY) {
console.error('OPENAI_API_KEY environment variable is required');
process.exit(1);
}
const accountId = process.env.STACKONE_ACCOUNT_ID;
/**
* Example 1: Search and execute with Vercel AI SDK
*
* The LLM receives only tool_search and tool_execute — two small tool definitions
* regardless of how many tools exist. It searches for what it needs and executes.
*/
const toolsWithAISDK = async (): Promise<void> => {
console.log('Example 1: Search and execute with Vercel AI SDK\n');
const toolset = new StackOneToolSet({
search: { method: 'semantic', topK: 3 },
...(accountId ? { accountId } : {}),
});
// Get search and execute tools — returns a Tools collection with tool_search + tool_execute
const accountIds = accountId ? [accountId] : [];
const tools = toolset.getTools({ accountIds });
console.log(
`Search and execute: ${tools
.toArray()
.map((t) => t.name)
.join(', ')}`,
);
console.log();
// Pass to the LLM — it will search for calendly tools, then execute
const { text, steps } = await generateText({
model: openai('gpt-5.4'),
tools: await tools.toAISDK(),
prompt: 'List my upcoming Calendly events for the next week.',
stopWhen: stepCountIs(10),
});
console.log('AI Response:', text);
console.log('\nSteps taken:');
for (const step of steps) {
for (const call of step.toolCalls ?? []) {
const args = (call as unknown as Record<string, unknown>).args;
const argsStr = args ? JSON.stringify(args).slice(0, 100) : '{}';
console.log(` - ${call.toolName}(${argsStr})`);
}
}
};
/**
* Example 2: Search and execute with OpenAI Chat Completions
*
* Same pattern, different framework. The search and execute tools convert to any format.
*/
const toolsWithOpenAI = async (): Promise<void> => {
console.log('\nExample 2: Search and execute with OpenAI Chat Completions\n');
const toolset = new StackOneToolSet({
search: { method: 'semantic', topK: 3 },
...(accountId ? { accountId } : {}),
});
const accountIds = accountId ? [accountId] : [];
const tools = toolset.getTools({ accountIds });
const openaiTools = tools.toOpenAI();
const client = new OpenAI();
const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
{
role: 'system',
content:
'You are a helpful scheduling assistant. Use tool_search to find relevant tools, then tool_execute to run them. Always read the parameter schemas from tool_search results carefully. If a tool needs a user URI, first search for and call a "get current user" tool to obtain it. If a tool execution fails, try different parameters or a different tool.',
},
{
role: 'user',
content: 'Check my upcoming Calendly events and list them.',
},
];
// Agent loop — let the LLM drive search and execution
const maxIterations = 10;
for (let i = 0; i < maxIterations; i++) {
const response = await client.chat.completions.create({
model: 'gpt-5.4',
messages,
tools: openaiTools,
tool_choice: 'auto',
});
const choice = response.choices[0];
if (!choice.message.tool_calls?.length) {
console.log('Final response:', choice.message.content);
break;
}
// Add assistant message with tool calls
messages.push(choice.message);
// Execute each tool call
for (const toolCall of choice.message.tool_calls) {
if (toolCall.type !== 'function') {
continue;
}
console.log(`LLM called: ${toolCall.function.name}(${toolCall.function.arguments})`);
const tool = tools.getTool(toolCall.function.name);
if (!tool) {
messages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify({ error: `Unknown tool: ${toolCall.function.name}` }),
});
continue;
}
const result = await tool.execute(toolCall.function.arguments);
messages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify(result),
});
}
}
};
// Main execution
const main = async (): Promise<void> => {
try {
await toolsWithAISDK();
await toolsWithOpenAI();
} catch (error) {
console.error('Error running examples:', error);
}
};
await main();
/**
* E2E test for ai-sdk-integration.ts example
*
* Tests the complete flow of using StackOne tools with the AI SDK.
*/
import { openai } from '@ai-sdk/openai';
import { generateText, stepCountIs } from 'ai';
import { TEST_BASE_URL } from '../mocks/constants';
import { StackOneToolSet } from '../src';
describe('ai-sdk-integration example e2e', () => {
beforeEach(() => {
vi.stubEnv('STACKONE_API_KEY', 'test-key');
vi.stubEnv('OPENAI_API_KEY', 'test-openai-key');
});
afterEach(() => {
vi.unstubAllEnvs();
});
it('should fetch tools, convert to AI SDK format, and generate text with tool calls', async () => {
const toolset = new StackOneToolSet({
accountId: 'your-bamboohr-account-id',
baseUrl: TEST_BASE_URL,
});
// Fetch all tools for this account via MCP
const tools = await toolset.fetchTools();
expect(tools.length).toBeGreaterThan(0);
// Convert to AI SDK tools
const aiSdkTools = await tools.toAISDK();
expect(aiSdkTools).toBeDefined();
expect(Object.keys(aiSdkTools).length).toBeGreaterThan(0);
// Verify the tools have the expected structure
const toolNames = Object.keys(aiSdkTools);
expect(toolNames).toContain('bamboohr_list_employees');
expect(toolNames).toContain('bamboohr_get_employee');
// The AI SDK will automatically call the tool if needed
const { text } = await generateText({
model: openai('gpt-5'),
tools: aiSdkTools,
prompt: 'Get all details about employee with id: c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA',
stopWhen: stepCountIs(3),
});
// The mocked OpenAI response includes 'Michael' in the text
expect(text).toContain('Michael');
});
});
/**
* Benchmark: measure SDK search latency with caching.
*
* Runs fetchTools, local (BM25+TF-IDF) search, and semantic search N times,
* reports cold vs warm average latency and the speedup from caching.
*
* Prerequisites:
* - STACKONE_API_KEY environment variable
* - STACKONE_ACCOUNT_ID environment variable
*
* Run with:
* STACKONE_API_KEY=xxx STACKONE_ACCOUNT_ID=xxx npx tsx examples/benchmark-search.ts
* STACKONE_API_KEY=xxx STACKONE_ACCOUNT_ID=xxx npx tsx examples/benchmark-search.ts --iterations 50
*/
import process from 'node:process';
import { StackOneToolSet } from '@stackone/ai';
const QUERIES = [
'list events',
'cancel a meeting',
'send a message',
'get current user',
'list employees',
];
function parseArgs(): number {
const idx = process.argv.indexOf('--iterations');
const idxShort = process.argv.indexOf('-n');
const pos = idx !== -1 ? idx : idxShort;
if (pos !== -1 && process.argv[pos + 1]) {
return Number.parseInt(process.argv[pos + 1], 10);
}
return 100;
}
async function bench(
fn: () => Promise<unknown>,
n: number,
): Promise<{ cold: number; warmAvg: number }> {
const times: number[] = [];
for (let i = 0; i < n; i++) {
const start = performance.now();
await fn();
times.push(performance.now() - start);
}
const cold = times[0];
const warmTimes = times.slice(1);
const warmAvg =
warmTimes.length > 0 ? warmTimes.reduce((a, b) => a + b, 0) / warmTimes.length : cold;
return { cold, warmAvg };
}
function fmtMs(ms: number): string {
return `${ms.toFixed(1)}ms`.padStart(10);
}
async function main(): Promise<void> {
const iterations = parseArgs();
const apiKey = process.env.STACKONE_API_KEY;
const accountId = process.env.STACKONE_ACCOUNT_ID;
if (!apiKey) {
console.error('Set STACKONE_API_KEY to run this benchmark.');
process.exit(1);
}
if (!accountId) {
console.error('Set STACKONE_ACCOUNT_ID to run this benchmark.');
process.exit(1);
}
console.log(
`Benchmarking with account ${accountId.slice(0, 8)}..., ${iterations} iterations each\n`,
);
const ts = new StackOneToolSet({
apiKey,
accountId,
search: { method: 'auto', topK: 5 },
});
const results: Array<{ name: string; cold: number; warmAvg: number; speedup: number }> = [];
let queryIdx = 0;
const nextQuery = (): string => QUERIES[queryIdx++ % QUERIES.length];
// --- 1. fetchTools ---
console.log(`[1/3] fetchTools x${iterations} ...`);
ts.clearCatalogCache();
const fetch = await bench(() => ts.fetchTools(), iterations);
const fetchSpeedup = fetch.cold / fetch.warmAvg;
results.push({ name: 'fetchTools', ...fetch, speedup: fetchSpeedup });
console.log(
` cold=${fmtMs(fetch.cold)} warm_avg=${fmtMs(fetch.warmAvg)} speedup=${fetchSpeedup.toFixed(0)}x`,
);
// --- 2. local search (BM25 + TF-IDF) ---
console.log(`[2/3] searchTools (local) x${iterations} ...`);
ts.clearCatalogCache();
queryIdx = 0;
const local = await bench(() => ts.searchTools(nextQuery(), { search: 'local' }), iterations);
const localSpeedup = local.cold / local.warmAvg;
results.push({ name: 'search (local/BM25)', ...local, speedup: localSpeedup });
console.log(
` cold=${fmtMs(local.cold)} warm_avg=${fmtMs(local.warmAvg)} speedup=${localSpeedup.toFixed(0)}x`,
);
// --- 3. semantic search (auto) ---
console.log(`[3/3] searchTools (semantic/auto) x${iterations} ...`);
ts.clearCatalogCache();
queryIdx = 0;
const semantic = await bench(() => ts.searchTools(nextQuery(), { search: 'auto' }), iterations);
const semanticSpeedup = semantic.cold / semantic.warmAvg;
results.push({ name: 'search (semantic)', ...semantic, speedup: semanticSpeedup });
console.log(
` cold=${fmtMs(semantic.cold)} warm_avg=${fmtMs(semantic.warmAvg)} speedup=${semanticSpeedup.toFixed(0)}x`,
);
// --- Summary ---
console.log('\n' + '='.repeat(65));
console.log(
`${'Benchmark'.padEnd(22)} ${'Cold'.padStart(10)} ${'Warm (avg)'.padStart(10)} ${'Speedup'.padStart(10)}`,
);
console.log('-'.repeat(65));
for (const r of results) {
console.log(
`${r.name.padEnd(22)} ${fmtMs(r.cold)} ${fmtMs(r.warmAvg)} ${`${r.speedup.toFixed(0)}x`.padStart(10)}`,
);
}
console.log('='.repeat(65));
console.log(`\nWarm = average of ${iterations - 1} calls after the first (cold) call.`);
console.log('Speedup = cold / warm_avg — shows the benefit of caching.\n');
}
void main();
/**
* E2E test for claude-agent-sdk-integration.ts example
*
* Tests the setup of StackOne tools with Claude Agent SDK.
*
* Note: The Claude Agent SDK spawns a subprocess to run claude-code, which
* requires the ANTHROPIC_API_KEY environment variable and a running claude-code
* installation. This test validates the tool setup and MCP server creation,
* but does not test the actual query execution.
*/
import { tool, createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk';
import { z } from 'zod';
import { TEST_BASE_URL } from '../mocks/constants';
import { StackOneToolSet } from '../src';
describe('claude-agent-sdk-integration example e2e', () => {
beforeEach(() => {
vi.stubEnv('STACKONE_API_KEY', 'test-key');
});
afterEach(() => {
vi.unstubAllEnvs();
});
it('should fetch tools and create Claude Agent SDK tool wrapper', async () => {
const toolset = new StackOneToolSet({
accountId: 'your-bamboohr-account-id',
baseUrl: TEST_BASE_URL,
});
// Fetch all tools for this account via MCP
const tools = await toolset.fetchTools();
expect(tools.length).toBeGreaterThan(0);
// Get a specific tool
const employeeTool = tools.getTool('bamboohr_get_employee');
expect(employeeTool).toBeDefined();
assert(employeeTool !== undefined);
// Create Claude Agent SDK tool from StackOne tool
const getEmployeeTool = tool(
employeeTool.name,
employeeTool.description,
{
id: z.string().describe('The employee ID'),
},
async (args) => {
const result = await employeeTool.execute(args);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result) }],
};
},
);
expect(getEmployeeTool.name).toBe('bamboohr_get_employee');
expect(getEmployeeTool.description).toContain('employee');
expect(getEmployeeTool.inputSchema).toHaveProperty('id');
expect(typeof getEmployeeTool.handler).toBe('function');
});
it('should create MCP server with StackOne tools', async () => {
const toolset = new StackOneToolSet({
accountId: 'your-bamboohr-account-id',
baseUrl: TEST_BASE_URL,
});
const tools = await toolset.fetchTools();
const employeeTool = tools.getTool('bamboohr_get_employee');
assert(employeeTool !== undefined);
// Create Claude Agent SDK tool
const getEmployeeTool = tool(
employeeTool.name,
employeeTool.description,
{
id: z.string().describe('The employee ID'),
},
async (args) => {
const result = await employeeTool.execute(args);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result) }],
};
},
);
// Create an MCP server with the StackOne tool
const mcpServer = createSdkMcpServer({
name: 'stackone-tools',
version: '1.0.0',
tools: [getEmployeeTool],
});
// Verify MCP server was created
expect(mcpServer).toBeDefined();
expect(mcpServer.name).toBe('stackone-tools');
expect(mcpServer.instance).toBeDefined();
});
it('should execute tool handler directly', async () => {
const toolset = new StackOneToolSet({
accountId: 'your-bamboohr-account-id',
baseUrl: TEST_BASE_URL,
});
const tools = await toolset.fetchTools();
const employeeTool = tools.getTool('bamboohr_get_employee');
assert(employeeTool !== undefined);
// Create Claude Agent SDK tool
const getEmployeeTool = tool(
employeeTool.name,
employeeTool.description,
{
id: z.string().describe('The employee ID'),
},
async (args) => {
const result = await employeeTool.execute(args);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result) }],
};
},
);
// Execute the tool handler directly
const result = await getEmployeeTool.handler(
{ id: 'c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA' },
{} as unknown,
);
expect(result).toBeDefined();
expect(result.content).toHaveLength(1);
expect(result.content[0]?.type).toBe('text');
// Parse the result text and verify it contains employee data
const textContent = result.content[0];
assert(textContent?.type === 'text');
const data = JSON.parse(textContent.text) as unknown;
expect(data).toHaveProperty('data');
});
});
/**
* Fetch Tools Debug CLI
*
* This example demonstrates how to build an interactive CLI tool using
* @clack/prompts to dynamically discover and execute StackOne tools.
*
* Features:
* - Interactive credential input with environment variable fallback
* - Dynamic tool discovery and selection
* - Spinner feedback during async operations
*
* Run with:
* ```bash
* npx tsx examples/fetch-tools-debug.ts
* ```
*/
import process from 'node:process';
import * as clack from '@clack/prompts';
import { StackOneToolSet } from '@stackone/ai';
/**
* Mask a sensitive value, showing only the first few and last few characters
*/
function maskValue(value: string, visibleStart = 4, visibleEnd = 4): string {
if (value.length <= visibleStart + visibleEnd) {
return '*'.repeat(value.length);
}
const start = value.slice(0, visibleStart);
const end = value.slice(-visibleEnd);
const masked = '*'.repeat(Math.min(value.length - visibleStart - visibleEnd, 8));
return `${start}${masked}${end}`;
}
clack.intro('Welcome to StackOne AI Tool Tester');
// Get API key
let apiKey: string;
const envApiKey = process.env.STACKONE_API_KEY;
if (envApiKey) {
const apiKeyChoice = await clack.select({
message: 'StackOne API Key:',
options: [
{ value: 'env', label: 'Use environment variable', hint: maskValue(envApiKey) },
{ value: 'input', label: 'Enter manually' },
],
});
if (clack.isCancel(apiKeyChoice)) {
clack.cancel('Operation cancelled');
process.exit(0);
}
if (apiKeyChoice === 'env') {
apiKey = envApiKey;
} else {
const apiKeyInput = await clack.text({
message: 'Enter your StackOne API key:',
placeholder: 'v1.us1.xxx...',
validate: (value) => {
if (!value) return 'API key is required';
},
});
if (clack.isCancel(apiKeyInput)) {
clack.cancel('Operation cancelled');
process.exit(0);
}
apiKey = apiKeyInput;
}
} else {
const apiKeyInput = await clack.text({
message: 'Enter your StackOne API key:',
placeholder: 'v1.us1.xxx...',
validate: (value) => {
if (!value) return 'API key is required';
},
});
if (clack.isCancel(apiKeyInput)) {
clack.cancel('Operation cancelled');
process.exit(0);
}
apiKey = apiKeyInput;
}
// Get base URL
let baseUrl: string;
const envBaseUrl = process.env.STACKONE_BASE_URL;
if (envBaseUrl) {
const baseUrlChoice = await clack.select({
message: 'StackOne Base URL:',
options: [
{ value: 'env', label: 'Use environment variable', hint: maskValue(envBaseUrl, 8, 8) },
{ value: 'input', label: 'Enter manually' },
],
});
if (clack.isCancel(baseUrlChoice)) {
clack.cancel('Operation cancelled');
process.exit(0);
}
if (baseUrlChoice === 'env') {
baseUrl = envBaseUrl;
} else {
const baseUrlInput = await clack.text({
message: 'Enter StackOne Base URL:',
placeholder: 'https://api.stackone.com',
defaultValue: 'https://api.stackone.com',
});
if (clack.isCancel(baseUrlInput)) {
clack.cancel('Operation cancelled');
process.exit(0);
}
baseUrl = baseUrlInput;
}
} else {
const baseUrlInput = await clack.text({
message: 'Enter StackOne Base URL (optional):',
placeholder: 'https://api.stackone.com',
defaultValue: 'https://api.stackone.com',
});
if (clack.isCancel(baseUrlInput)) {
clack.cancel('Operation cancelled');
process.exit(0);
}
baseUrl = baseUrlInput;
}
// Get account ID
let accountId: string;
const envAccountId = process.env.STACKONE_ACCOUNT_ID;
if (envAccountId) {
const accountIdChoice = await clack.select({
message: 'StackOne Account ID:',
options: [
{ value: 'env', label: 'Use environment variable', hint: maskValue(envAccountId) },
{ value: 'input', label: 'Enter manually' },
],
});
if (clack.isCancel(accountIdChoice)) {
clack.cancel('Operation cancelled');
process.exit(0);
}
if (accountIdChoice === 'env') {
accountId = envAccountId;
} else {
const accountIdInput = await clack.text({
message: 'Enter your StackOne Account ID:',
placeholder: 'acc_xxx...',
validate: (value) => {
if (!value) return 'Account ID is required';
},
});
if (clack.isCancel(accountIdInput)) {
clack.cancel('Operation cancelled');
process.exit(0);
}
accountId = accountIdInput as string;
}
} else {
const accountIdInput = await clack.text({
message: 'Enter your StackOne Account ID:',
placeholder: 'acc_xxx...',
validate: (value) => {
if (!value) return 'Account ID is required';
},
});
if (clack.isCancel(accountIdInput)) {
clack.cancel('Operation cancelled');
process.exit(0);
}
accountId = accountIdInput as string;
}
// @ts-expect-error Bun global is not in Node.js types
if ((typeof globalThis.Bun as any) !== 'undefined') {
const detailedLog = await clack.confirm({
message: 'Enable detailed logging? (recommended for Bun.js users)',
});
if (clack.isCancel(detailedLog)) {
clack.cancel('Operation cancelled');
process.exit(0);
}
if (detailedLog) {
process.env.BUN_CONFIG_VERBOSE_FETCH = 'curl';
}
}
const spinner = clack.spinner();
spinner.start('Initializing StackOne client...');
const toolset = new StackOneToolSet({
apiKey,
baseUrl,
accountId,
});
spinner.message('Fetching available tools...');
const tools = await toolset.fetchTools();
const allTools = tools.toArray();
spinner.stop(`Found ${allTools.length} tools`);
// Select a tool interactively
const selectedToolName = await clack.select({
message: 'Select a tool to execute:',
options: allTools.map((tool) => ({
label: tool.description,
value: tool.name,
hint: tool.name,
})),
});
if (clack.isCancel(selectedToolName)) {
clack.cancel('Operation cancelled');
process.exit(0);
}
const selectedTool = tools.getTool(selectedToolName as string);
if (!selectedTool) {
clack.log.error(`Tool '${selectedToolName}' not found!`);
process.exit(1);
}
spinner.start(`Executing: ${selectedTool.description}`);
try {
const result = await selectedTool.execute({
query: { limit: 5 },
});
spinner.stop('Execution complete');
clack.log.success('Result:');
// Display result based on its structure
if (Array.isArray(result)) {
// For array results, use console.table for better readability
if (result.length > 0 && typeof result[0] === 'object') {
console.table(result);
} else {
console.log(result);
}
} else if (result && typeof result === 'object') {
// Check if result has a data array property (common API response pattern)
const data = (result as Record<string, unknown>).data;
if (Array.isArray(data) && data.length > 0 && typeof data[0] === 'object') {
console.log('\nData:');
console.table(data);
// Show other properties
const otherProps = Object.fromEntries(
Object.entries(result as Record<string, unknown>).filter(([key]) => key !== 'data'),
);
if (Object.keys(otherProps).length > 0) {
console.log('\nMetadata:');
console.log(JSON.stringify(otherProps, null, 2));
}
} else {
console.log(JSON.stringify(result, null, 2));
}
} else {
console.log(result);
}
clack.outro('Done!');
} catch (error) {
spinner.stop('Execution failed');
if (error instanceof Error) {
clack.log.error(`Error: ${error.message}`);
if (error.cause) {
clack.log.info(`Cause: ${JSON.stringify(error.cause, null, 2)}`);
}
if (error.stack) {
clack.log.info(`Stack trace:\n${error.stack}`);
}
} else {
clack.log.error(`Error: ${JSON.stringify(error, null, 2)}`);
}
clack.outro('Failed');
process.exit(1);
}
/**
* E2E test for fetch-tools.ts example
*
* Tests the complete flow of fetching and filtering tools via MCP.
*/
import { http, HttpResponse } from 'msw';
import { server } from '../mocks/node';
import { TEST_BASE_URL } from '../mocks/constants';
import { StackOneToolSet } from '../src';
describe('fetch-tools example e2e', () => {
beforeEach(() => {
vi.stubEnv('STACKONE_API_KEY', 'test-key');
});
afterEach(() => {
vi.unstubAllEnvs();
});
it('should fetch tools, filter by various criteria, and execute a tool', async () => {
// Setup RPC handler for tool execution
server.use(
http.post(`${TEST_BASE_URL}/actions/rpc`, async ({ request }) => {
const body: unknown = await request.json();
assert(typeof body === 'object' && body !== null);
const { action } = body as Record<string, unknown>;
if (action === 'bamboohr_list_employees') {
return HttpResponse.json({
data: [
{ id: '1', name: 'Employee 1' },
{ id: '2', name: 'Employee 2' },
{ id: '3', name: 'Employee 3' },
{ id: '4', name: 'Employee 4' },
{ id: '5', name: 'Employee 5' },
],
});
}
return HttpResponse.json({ data: {} });
}),
);
const toolset = new StackOneToolSet({
baseUrl: TEST_BASE_URL,
});
// Example 1: Fetch all tools (without account filter)
const allTools = await toolset.fetchTools();
expect(allTools.length).toBeGreaterThan(0);
// Example 2: Filter by account IDs using setAccounts()
toolset.setAccounts(['your-bamboohr-account-id']);
const toolsByAccounts = await toolset.fetchTools();
expect(toolsByAccounts.length).toBeGreaterThan(0);
// Example 3: Filter by account IDs using options
const toolsByAccountsOption = await toolset.fetchTools({
accountIds: ['your-bamboohr-account-id'],
});
expect(toolsByAccountsOption.length).toBeGreaterThan(0);
// Example 4: Filter by providers
const toolsByProviders = await toolset.fetchTools({
accountIds: ['your-bamboohr-account-id'],
providers: ['bamboohr'],
});
expect(toolsByProviders.length).toBeGreaterThan(0);
const providerToolNames = toolsByProviders.toArray().map((t) => t.name);
expect(
providerToolNames.every((name) => name.startsWith('bamboohr_') || name.startsWith('tool_')),
).toBe(true);
// Example 5: Filter by actions with exact match
const toolsByActions = await toolset.fetchTools({
accountIds: ['your-bamboohr-account-id'],
actions: ['bamboohr_list_employees', 'bamboohr_create_employee'],
});
const actionToolNames = toolsByActions.toArray().map((t) => t.name);
expect(actionToolNames).toContain('bamboohr_list_employees');
expect(actionToolNames).toContain('bamboohr_create_employee');
// Example 6: Filter by actions with glob pattern
const toolsByGlobPattern = await toolset.fetchTools({
accountIds: ['your-bamboohr-account-id'],
actions: ['*_list_employees'],
});
const globToolNames = toolsByGlobPattern
.toArray()
.filter((t) => !t.name.startsWith('tool_'))
.map((t) => t.name);
expect(globToolNames).toContain('bamboohr_list_employees');
// Execute a tool
const tool = toolsByAccounts.getTool('bamboohr_list_employees');
expect(tool).toBeDefined();
const result = await tool!.execute({
query: { limit: 5 },
});
expect(result.data).toBeDefined();
expect(Array.isArray(result.data)).toBe(true);
});
});
/**
* Example: fetch the latest StackOne tool catalog with filtering options.
*
* Set `STACKONE_API_KEY` before running.
* By default the script exits early in test environments where a real key is
* not available.
*/
import process from 'node:process';
import { StackOneToolSet } from '@stackone/ai';
const apiKey = process.env.STACKONE_API_KEY;
if (!apiKey) {
console.error('STACKONE_API_KEY environment variable is required');
process.exit(1);
}
const toolset = new StackOneToolSet({});
// Example 1: Fetch all tools
console.log('\n=== Example 1: Fetch all tools ===');
const allTools = await toolset.fetchTools();
console.log(`Loaded ${allTools.length} tools`);
// Example 2: Filter by account IDs using setAccounts()
console.log('\n=== Example 2: Filter by account IDs (using setAccounts) ===');
toolset.setAccounts(['account-123', 'account-456']);
const toolsByAccounts = await toolset.fetchTools();
console.log(`Loaded ${toolsByAccounts.length} tools for specified accounts`);
// Example 3: Filter by account IDs using options
console.log('\n=== Example 3: Filter by account IDs (using options) ===');
const toolsByAccountsOption = await toolset.fetchTools({
accountIds: ['your-account-id'],
});
console.log(`Loaded ${toolsByAccountsOption.length} tools for your-account-id`);
// Example 4: Filter by providers
console.log('\n=== Example 4: Filter by providers ===');
const toolsByProviders = await toolset.fetchTools({
providers: ['hibob', 'bamboohr'],
});
console.log(`Loaded ${toolsByProviders.length} tools for HiBob and BambooHR`);
// Example 5: Filter by actions with exact match
console.log('\n=== Example 5: Filter by actions (exact match) ===');
const toolsByActions = await toolset.fetchTools({
actions: ['bamboohr_list_employees', 'bamboohr_create_employee'],
});
console.log(`Loaded ${toolsByActions.length} tools matching exact action names`);
// Example 6: Filter by actions with glob pattern
console.log('\n=== Example 6: Filter by actions (glob pattern) ===');
const toolsByGlobPattern = await toolset.fetchTools({
actions: ['*_list_employees'],
});
console.log(`Loaded ${toolsByGlobPattern.length} tools matching *_list_employees pattern`);
// Example 7: Combine multiple filters
console.log('\n=== Example 7: Combine multiple filters ===');
const toolsCombined = await toolset.fetchTools({
accountIds: ['account-123'],
providers: ['hibob'],
actions: ['*_list_*'],
});
console.log(
`Loaded ${toolsCombined.length} tools for account-123, provider hibob, matching *_list_* pattern`,
);
// Execute a tool
console.log('\n=== Executing a tool ===');
const tool = allTools.getTool('bamboohr_list_employees');
if (!tool) {
throw new Error('Tool bamboohr_list_employees not found in the catalog');
}
const result = await tool.execute({
query: { limit: 5 },
});
console.log('Sample execution result:', JSON.stringify(result, null, 2));
/**
* E2E test for openai-integration.ts example
*
* Tests the complete flow of using StackOne tools with OpenAI Chat Completions API.
*/
import OpenAI from 'openai';
import { TEST_BASE_URL } from '../mocks/constants';
import { StackOneToolSet } from '../src';
describe('openai-integration example e2e', () => {
beforeEach(() => {
vi.stubEnv('STACKONE_API_KEY', 'test-key');
vi.stubEnv('OPENAI_API_KEY', 'test-openai-key');
});
afterEach(() => {
vi.unstubAllEnvs();
});
it('should fetch tools, convert to OpenAI format, and create chat completion with tool calls', async () => {
const toolset = new StackOneToolSet({
accountId: 'your-bamboohr-account-id',
baseUrl: TEST_BASE_URL,
});
// Fetch all tools for this account via MCP
const tools = await toolset.fetchTools();
const openAITools = tools.toOpenAI();
// Verify tools are in OpenAI format
expect(Array.isArray(openAITools)).toBe(true);
expect(openAITools.length).toBeGreaterThan(0);
expect(openAITools[0]).toHaveProperty('type', 'function');
expect(openAITools[0]).toHaveProperty('function');
// Initialize OpenAI client
const openai = new OpenAI();
// Create a chat completion with tool calls
const response = await openai.chat.completions.create({
model: 'gpt-5',
messages: [
{
role: 'system',
content: 'You are a helpful assistant that can access BambooHR information.',
},
{
role: 'user',
content:
'What is the employee with id: c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA phone number?',
},
],
tools: openAITools,
});
// Verify the response contains tool calls
expect(response.choices.length).toBeGreaterThan(0);
const choice = response.choices[0];
expect(choice.message.tool_calls).toBeDefined();
expect(choice.message.tool_calls!.length).toBeGreaterThan(0);
const toolCall = choice.message.tool_calls![0];
assert(toolCall.type === 'function');
expect(toolCall.function.name).toBe('bamboohr_get_employee');
// Parse the arguments to verify they contain the expected fields
const args: unknown = JSON.parse(toolCall.function.arguments);
assert(typeof args === 'object' && args !== null && 'id' in args);
expect(args.id).toBe('c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA');
});
});
/**
* E2E test for openai-responses-integration.ts example
*
* Tests the complete flow of using StackOne tools with OpenAI Responses API.
*/
import OpenAI from 'openai';
import { TEST_BASE_URL } from '../mocks/constants';
import { StackOneToolSet } from '../src';
describe('openai-responses-integration example e2e', () => {
beforeEach(() => {
vi.stubEnv('STACKONE_API_KEY', 'test-key');
vi.stubEnv('OPENAI_API_KEY', 'test-openai-key');
});
afterEach(() => {
vi.unstubAllEnvs();
});
it('should fetch tools, convert to OpenAI Responses format, and create response with tool calls', async () => {
const toolset = new StackOneToolSet({
accountId: 'your-stackone-account-id',
baseUrl: TEST_BASE_URL,
});
// Fetch tools via MCP with action filter
const tools = await toolset.fetchTools({
actions: ['*_list_*'],
});
const openAIResponsesTools = tools.toOpenAIResponses();
// Verify tools are in OpenAI Responses format
expect(Array.isArray(openAIResponsesTools)).toBe(true);
expect(openAIResponsesTools.length).toBeGreaterThan(0);
// Initialize OpenAI client
const openai = new OpenAI();
// Create a response with tool calls using the Responses API
const response = await openai.responses.create({
model: 'gpt-5',
instructions: 'You are a helpful assistant that can access various tools.',
input: 'What is the employee with id: c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA phone number?',
tools: openAIResponsesTools,
});
// Verify the response contains expected data
expect(response.id).toBeDefined();
expect(response.model).toBeDefined();
// Check if the model made any tool calls
const toolCalls = response.output.filter(
(item): item is OpenAI.Responses.ResponseFunctionToolCall => item.type === 'function_call',
);
expect(toolCalls.length).toBeGreaterThan(0);
const toolCall = toolCalls[0];
expect(toolCall.name).toBe('bamboohr_get_employee');
// Parse the arguments to verify they contain the expected fields
const args = JSON.parse(toolCall.arguments);
expect(args.id).toBe('c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA');
});
});
/**
* E2E test for tanstack-ai-integration.ts example
*
* Tests the complete flow of using StackOne tools with TanStack AI.
*
* Note: TanStack AI requires Zod schemas for tool input validation.
* This test validates tool setup and schema conversion, but the actual
* chat() call requires Zod schemas which are not directly exposed by
* StackOne tools.
*/
import { TEST_BASE_URL } from '../mocks/constants';
import { StackOneToolSet } from '../src';
describe('tanstack-ai-integration example e2e', () => {
beforeEach(() => {
vi.stubEnv('STACKONE_API_KEY', 'test-key');
vi.stubEnv('OPENAI_API_KEY', 'test-openai-key');
});
afterEach(() => {
vi.unstubAllEnvs();
});
it('should fetch tools and convert to TanStack AI format', async () => {
const toolset = new StackOneToolSet({
accountId: 'your-bamboohr-account-id',
baseUrl: TEST_BASE_URL,
});
// Fetch all tools for this account via MCP
const tools = await toolset.fetchTools();
expect(tools.length).toBeGreaterThan(0);
// Get a specific tool
const employeeTool = tools.getTool('bamboohr_get_employee');
assert(employeeTool, 'Expected bamboohr_get_employee tool to exist');
// Create TanStack AI compatible tool wrapper
// Use toJsonSchema() to get the parameter schema in JSON Schema format
const getEmployeeTool = {
name: employeeTool.name,
description: employeeTool.description,
inputSchema: employeeTool.toJsonSchema(),
execute: employeeTool.execute.bind(employeeTool),
};
expect(getEmployeeTool.name).toBe('bamboohr_get_employee');
expect(getEmployeeTool.inputSchema.type).toBe('object');
});
it('should execute tool directly', async () => {
const toolset = new StackOneToolSet({
accountId: 'your-bamboohr-account-id',
baseUrl: TEST_BASE_URL,
});
const tools = await toolset.fetchTools();
const employeeTool = tools.getTool('bamboohr_get_employee');
assert(employeeTool !== undefined, 'Expected to find bamboohr_get_employee tool');
// Create TanStack AI compatible tool wrapper
const getEmployeeTool = {
name: employeeTool.name,
description: employeeTool.description,
inputSchema: employeeTool.toJsonSchema(),
execute: employeeTool.execute.bind(employeeTool),
};
// Execute the tool directly to verify it works
const result = await getEmployeeTool.execute({
id: 'c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA',
});
expect(result).toBeDefined();
expect(result).toHaveProperty('data');
});
});
/**
* This example shows how to use StackOne tools with TanStack AI.
*
* TanStack AI requires Zod schemas for tool input validation.
* This example demonstrates how to wrap StackOne tools for use with TanStack AI
* by creating Zod schemas that match the tool's JSON Schema.
*/
import assert from 'node:assert';
import process from 'node:process';
import { chat } from '@tanstack/ai';
import { openaiText } from '@tanstack/ai-openai';
import { z } from 'zod';
import { StackOneToolSet } from '@stackone/ai';
const apiKey = process.env.STACKONE_API_KEY;
if (!apiKey) {
console.error('STACKONE_API_KEY environment variable is required');
process.exit(1);
}
const tanstackAiIntegration = async (): Promise<void> => {
// Initialize StackOne — reads STACKONE_API_KEY and STACKONE_ACCOUNT_ID from env
const toolset = new StackOneToolSet();
// Fetch tools from StackOne
const tools = await toolset.fetchTools();
// Get a specific tool and create a TanStack AI compatible tool
const employeeTool = tools.getTool('bamboohr_get_employee');
assert(employeeTool !== undefined, 'Expected to find bamboohr_get_employee tool');
// Create a TanStack AI server tool from the StackOne tool
// TanStack AI requires Zod schemas, so we create one that matches the tool's parameters
const getEmployeeTool = {
name: employeeTool.name,
description: employeeTool.description,
// TanStack AI requires Zod schema for input validation
inputSchema: z.object({
id: z.string().describe('The employee ID'),
}),
execute: async (args: { id: string }) => {
return employeeTool.execute(args);
},
};
// Use TanStack AI chat with the tool
// The adapter reads OPENAI_API_KEY from the environment automatically
const adapter = openaiText('gpt-5');
const stream = chat({
adapter,
messages: [
{
role: 'user',
content: 'Get the employee with id: c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA',
},
],
tools: [getEmployeeTool],
});
// Process the stream using AG-UI protocol events
let hasToolCall = false;
for await (const chunk of stream) {
if (chunk.type === 'TOOL_CALL_START') {
hasToolCall = true;
assert(
chunk.toolName === 'bamboohr_get_employee',
'Expected tool call to be bamboohr_get_employee',
);
}
}
assert(hasToolCall, 'Expected at least one tool call');
};
await tanstackAiIntegration();
/**
* Workday integration: timeout and account scoping for slow providers.
*
* Workday can take 10-15s to respond. This example shows how to configure
* timeout for slow providers.
*
* Prerequisites:
* - STACKONE_API_KEY
* - STACKONE_ACCOUNT_ID (a Workday-connected account)
* - OPENAI_API_KEY
*
* Run with:
* STACKONE_API_KEY=xxx OPENAI_API_KEY=xxx STACKONE_ACCOUNT_ID=xxx npx tsx examples/workday-integration.ts
*/
import process from 'node:process';
import { StackOneToolSet } from '@stackone/ai';
import OpenAI from 'openai';
const apiKey = process.env.STACKONE_API_KEY ?? '';
const accountId = process.env.STACKONE_ACCOUNT_ID ?? '';
if (!apiKey) {
console.error('Set STACKONE_API_KEY to run this example.');
process.exit(1);
}
if (!accountId) {
console.error('Set STACKONE_ACCOUNT_ID to run this example.');
process.exit(1);
}
if (!process.env.OPENAI_API_KEY) {
console.error('Set OPENAI_API_KEY to run this example.');
process.exit(1);
}
// Timeout for slow providers (Workday can take 10-15s)
const toolset = new StackOneToolSet({
apiKey,
accountId,
search: { method: 'auto', topK: 5 },
timeout: 120_000,
});
const client = new OpenAI();
async function runAgent(
messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[],
tools: OpenAI.Chat.Completions.ChatCompletionTool[],
maxSteps = 10,
): Promise<void> {
for (let i = 0; i < maxSteps; i++) {
const response = await client.chat.completions.create({ model: 'gpt-5.4', messages, tools });
const choice = response.choices[0];
if (!choice.message.tool_calls?.length) {
console.log(`Answer: ${choice.message.content}`);
return;
}
messages.push(choice.message);
for (const tc of choice.message.tool_calls) {
if (tc.type !== 'function') continue;
console.log(` -> ${tc.function.name}(${tc.function.arguments.slice(0, 80)})`);
const searchTools = toolset.getTools({ accountIds: [accountId] });
const tool = searchTools.getTool(tc.function.name);
const result = tool ? await tool.execute(tc.function.arguments) : { error: 'Unknown tool' };
messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(result) });
}
}
}
// --- Example 1: Search and execute mode ---
console.log('=== Search and execute mode ===\n');
const searchTools = toolset.getTools({ accountIds: [accountId] }).toOpenAI();
await runAgent(
[
{ role: 'system', content: 'Use tool_search to find tools, then tool_execute to run them.' },
{ role: 'user', content: 'List the first 5 employees.' },
],
searchTools,
);
// --- Example 2: Normal mode ---
console.log('\n=== Normal mode ===\n');
const tools = await toolset.fetchTools({ actions: ['workday_*_employee*'] });
if (tools.length === 0) {
console.log('No Workday tools found for this account.');
} else {
await runAgent([{ role: 'user', content: 'List the first 5 employees.' }], tools.toOpenAI());
}