From c37de3c85d3ebb2c40183c56302ca749b5c40f37 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Thu, 11 Dec 2025 14:59:54 +0000 Subject: [PATCH 01/24] chore: remove unused sample-document --- examples/sample-document.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 examples/sample-document.txt diff --git a/examples/sample-document.txt b/examples/sample-document.txt deleted file mode 100644 index 3a0f1d0..0000000 --- a/examples/sample-document.txt +++ /dev/null @@ -1 +0,0 @@ -This is an experimental document handling test file. \ No newline at end of file From e7372a403e21fbcea01e8a6e671ce2d1f1b78df6 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Thu, 11 Dec 2025 15:00:02 +0000 Subject: [PATCH 02/24] chore: remove fetch tools example --- examples/fetch-tools.ts | 82 ----------------------------------------- 1 file changed, 82 deletions(-) delete mode 100644 examples/fetch-tools.ts diff --git a/examples/fetch-tools.ts b/examples/fetch-tools.ts deleted file mode 100644 index de154b7..0000000 --- a/examples/fetch-tools.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Example: fetch the latest StackOne tool catalog with filtering options. - * - * Set `STACKONE_API_KEY` (and optionally `STACKONE_BASE_URL`) 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({ - baseUrl: process.env.STACKONE_BASE_URL ?? 'https://api.stackone.com', -}); - -// 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: ['account-789'], -}); -console.log(`Loaded ${toolsByAccountsOption.length} tools for account-789`); - -// 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: ['hris_list_employees', 'hris_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('hris_list_employees'); -if (!tool) { - throw new Error('Tool hris_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)); From 3b59d18c7cb2d2cfb434415f3b81932a88945464 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Thu, 11 Dec 2025 15:00:10 +0000 Subject: [PATCH 03/24] Revert "chore: remove fetch tools example" This reverts commit e7372a403e21fbcea01e8a6e671ce2d1f1b78df6. --- examples/fetch-tools.ts | 82 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 examples/fetch-tools.ts diff --git a/examples/fetch-tools.ts b/examples/fetch-tools.ts new file mode 100644 index 0000000..de154b7 --- /dev/null +++ b/examples/fetch-tools.ts @@ -0,0 +1,82 @@ +/** + * Example: fetch the latest StackOne tool catalog with filtering options. + * + * Set `STACKONE_API_KEY` (and optionally `STACKONE_BASE_URL`) 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({ + baseUrl: process.env.STACKONE_BASE_URL ?? 'https://api.stackone.com', +}); + +// 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: ['account-789'], +}); +console.log(`Loaded ${toolsByAccountsOption.length} tools for account-789`); + +// 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: ['hris_list_employees', 'hris_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('hris_list_employees'); +if (!tool) { + throw new Error('Tool hris_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)); From 619c637598e5615d4fdb687a1ef1b8c794452e4a Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Thu, 11 Dec 2025 15:24:36 +0000 Subject: [PATCH 04/24] feat(example): update interactive fetchTool debug --- examples/fetch-tools-debug.ts | 297 ++++++++++++++++++++++++++++++++++ examples/interactive-cli.ts | 160 ------------------ 2 files changed, 297 insertions(+), 160 deletions(-) create mode 100644 examples/fetch-tools-debug.ts delete mode 100644 examples/interactive-cli.ts diff --git a/examples/fetch-tools-debug.ts b/examples/fetch-tools-debug.ts new file mode 100644 index 0000000..bd140b2 --- /dev/null +++ b/examples/fetch-tools-debug.ts @@ -0,0 +1,297 @@ +/** + * Interactive CLI Demo + * + * 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/interactive-cli.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('Initialising 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).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).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); +} diff --git a/examples/interactive-cli.ts b/examples/interactive-cli.ts deleted file mode 100644 index cf36482..0000000 --- a/examples/interactive-cli.ts +++ /dev/null @@ -1,160 +0,0 @@ -/** - * Interactive CLI Demo - * - * 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/interactive-cli.ts - * ``` - */ - -import process from 'node:process'; -import * as clack from '@clack/prompts'; -import { StackOneToolSet } from '@stackone/ai'; - -// Enable verbose fetch logging when running with Bun -process.env.BUN_CONFIG_VERBOSE_FETCH = 'curl'; - -clack.intro('Welcome to StackOne AI Tool Tester'); - -// Check if environment variables are available -const hasEnvVars = process.env.STACKONE_API_KEY && process.env.STACKONE_ACCOUNT_ID; - -let apiKey: string; -let baseUrl: string; -let accountId: string; - -if (hasEnvVars) { - const useEnv = await clack.confirm({ - message: 'Use environment variables from .env file?', - }); - - if (clack.isCancel(useEnv)) { - clack.cancel('Operation cancelled'); - process.exit(0); - } - - if (useEnv) { - apiKey = process.env.STACKONE_API_KEY!; - baseUrl = process.env.STACKONE_BASE_URL || 'https://api.stackone.com'; - accountId = process.env.STACKONE_ACCOUNT_ID!; - } else { - const credentials = await promptCredentials(); - apiKey = credentials.apiKey; - baseUrl = credentials.baseUrl; - accountId = credentials.accountId; - } -} else { - const credentials = await promptCredentials(); - apiKey = credentials.apiKey; - baseUrl = credentials.baseUrl; - accountId = credentials.accountId; -} - -async function promptCredentials(): Promise<{ - apiKey: string; - baseUrl: string; - accountId: string; -}> { - 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); - } - - 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); - } - - 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); - } - - return { - apiKey: apiKeyInput as string, - baseUrl: baseUrlInput as string, - accountId: accountIdInput as string, - }; -} - -const spinner = clack.spinner(); -spinner.start('Initialising 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:'); - console.log(JSON.stringify(result, null, 2)); - clack.outro('Done!'); -} catch (error) { - spinner.stop('Execution failed'); - clack.log.error(error instanceof Error ? error.message : String(error)); - clack.outro('Failed'); - process.exit(1); -} From acac6b99f0f00a6e39e2553dd6262666317a2ef9 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:01:19 +0000 Subject: [PATCH 05/24] refactor(types): replace json-schema library with custom JSONSchema type - Define comprehensive JSONSchema interface in types.ts covering all JSON Schema draft-07 properties - Add toJsonSchema() method to BaseTool and Tools classes for framework-agnostic schema export - Refactor toOpenAI(), toAnthropic(), and toOpenAIResponses() to use toJsonSchema() internally, reducing code duplication - Use type-fest OverrideProperties for ObjectJSONSchema type to ensure type: 'object' is always set - Remove json-schema and @types/json-schema dependencies This reduces external dependencies while providing a more flexible JSONSchema type that works seamlessly with OpenAI, Anthropic, and other LLM providers. --- package.json | 2 -- pnpm-lock.yaml | 20 ----------------- pnpm-workspace.yaml | 2 -- src/tool.test.ts | 21 ++++++++++-------- src/tool.ts | 49 ++++++++++++++++++++++++++++++------------ src/types.ts | 52 +++++++++++++++++++++++++++++++++++++++++---- 6 files changed, 96 insertions(+), 50 deletions(-) diff --git a/package.json b/package.json index 0b11c50..e2e5147 100644 --- a/package.json +++ b/package.json @@ -38,13 +38,11 @@ "@modelcontextprotocol/sdk": "catalog:prod", "@orama/orama": "catalog:prod", "defu": "catalog:prod", - "json-schema": "catalog:prod", "zod": "catalog:dev" }, "devDependencies": { "@ai-sdk/provider-utils": "catalog:dev", "@hono/mcp": "catalog:dev", - "@types/json-schema": "catalog:dev", "@types/node": "catalog:dev", "@typescript/native-preview": "catalog:dev", "@vitest/coverage-v8": "catalog:dev", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 935fb0b..07a8f3d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,9 +18,6 @@ catalogs: '@hono/mcp': specifier: ^0.1.4 version: 0.1.5 - '@types/json-schema': - specifier: ^7.0.15 - version: 7.0.15 '@types/node': specifier: ^22.13.5 version: 22.19.1 @@ -89,9 +86,6 @@ catalogs: defu: specifier: ^6.1.4 version: 6.1.4 - json-schema: - specifier: ^0.4.0 - version: 0.4.0 importers: .: @@ -108,9 +102,6 @@ importers: defu: specifier: catalog:prod version: 6.1.4 - json-schema: - specifier: catalog:prod - version: 0.4.0 zod: specifier: catalog:dev version: 4.1.13 @@ -121,9 +112,6 @@ importers: '@hono/mcp': specifier: catalog:dev version: 0.1.5(@modelcontextprotocol/sdk@1.24.3(zod@4.1.13))(hono@4.10.7) - '@types/json-schema': - specifier: catalog:dev - version: 7.0.15 '@types/node': specifier: catalog:dev version: 22.19.1 @@ -1676,12 +1664,6 @@ packages: integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==, } - '@types/json-schema@7.0.15': - resolution: - { - integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==, - } - '@types/node@22.19.1': resolution: { @@ -4330,8 +4312,6 @@ snapshots: '@types/estree@1.0.8': {} - '@types/json-schema@7.0.15': {} - '@types/node@22.19.1': dependencies: undici-types: 6.21.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f2c4844..49c0e40 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -10,7 +10,6 @@ catalogs: '@clack/prompts': ^0.11.0 '@ai-sdk/provider-utils': ^3.0.18 '@hono/mcp': ^0.1.4 - '@types/json-schema': ^7.0.15 '@types/node': ^22.13.5 '@typescript/native-preview': ^7.0.0-dev.20251209.1 '@vitest/coverage-v8': ^4.0.15 @@ -35,7 +34,6 @@ catalogs: '@modelcontextprotocol/sdk': ^1.24.3 '@orama/orama': ^3.1.11 defu: ^6.1.4 - json-schema: ^0.4.0 enablePrePostScripts: true diff --git a/src/tool.test.ts b/src/tool.test.ts index 9a70e44..b1961da 100644 --- a/src/tool.test.ts +++ b/src/tool.test.ts @@ -1,7 +1,11 @@ import { jsonSchema } from 'ai'; -import type { JSONSchema7 } from 'json-schema'; import { BaseTool, type MetaToolSearchResult, StackOneTool, Tools } from './tool'; -import { type ExecuteConfig, ParameterLocation, type ToolParameters } from './types'; +import { + type ExecuteConfig, + type JSONSchema, + ParameterLocation, + type ToolParameters, +} from './types'; import { StackOneAPIError } from './utils/errors'; // Create a mock tool for testing @@ -1107,11 +1111,10 @@ describe('Schema Validation', () => { ); const parameters = tool.toOpenAI().function.parameters; - expect(parameters).toBeDefined(); - const properties = parameters?.properties as Record; + const properties = parameters?.properties as Record; expect(properties.arrayWithItems.items).toBeDefined(); - expect((properties.arrayWithItems.items as JSONSchema7).type).toBe('number'); + expect((properties.arrayWithItems.items as JSONSchema).type).toBe('number'); }); it('should handle nested object structure', () => { @@ -1144,7 +1147,7 @@ describe('Schema Validation', () => { const parameters = tool.toOpenAI().function.parameters; expect(parameters).toBeDefined(); - const properties = parameters?.properties as Record; + const properties = parameters?.properties as Record; const nestedObject = properties.nestedObject; expect(nestedObject.type).toBe('object'); @@ -1185,7 +1188,7 @@ describe('Schema Validation', () => { // @ts-ignore - jsonSchema is available on Schema wrapper from ai sdk const arrayWithItems = toolObj.inputSchema.jsonSchema.properties?.arrayWithItems; expect(arrayWithItems?.type).toBe('array'); - expect((arrayWithItems?.items as JSONSchema7)?.type).toBe('string'); + expect((arrayWithItems?.items as JSONSchema)?.type).toBe('string'); }); it('should handle nested filter object for AI SDK', async () => { @@ -1220,14 +1223,14 @@ describe('Schema Validation', () => { const parameters = tool.toOpenAI().function.parameters; expect(parameters).toBeDefined(); - const aiSchema = jsonSchema(parameters as JSONSchema7); + const aiSchema = jsonSchema(parameters as JSONSchema); expect(aiSchema).toBeDefined(); const aiSdkTool = await tool.toAISDK(); // TODO: Remove ts-ignore once AISDKToolDefinition properly types inputSchema.jsonSchema // @ts-ignore - jsonSchema is available on Schema wrapper from ai sdk const filterProp = aiSdkTool[tool.name].inputSchema.jsonSchema.properties?.filter as - | (JSONSchema7 & { properties: Record }) + | (JSONSchema & { properties: Record }) | undefined; expect(filterProp?.type).toBe('object'); diff --git a/src/tool.ts b/src/tool.ts index 221e121..03b4fbf 100644 --- a/src/tool.ts +++ b/src/tool.ts @@ -2,6 +2,7 @@ import type { Tool as AnthropicTool } from '@anthropic-ai/sdk/resources'; import * as orama from '@orama/orama'; import type { ChatCompletionFunctionTool } from 'openai/resources/chat/completions'; import type { FunctionTool as OpenAIResponsesFunctionTool } from 'openai/resources/responses/responses'; +import type { OverrideProperties } from 'type-fest'; import { DEFAULT_HYBRID_ALPHA } from './consts'; import { RequestBuilder } from './requestBuilder'; import type { @@ -13,14 +14,22 @@ import type { Experimental_ToolCreationOptions, HttpExecuteConfig, JsonDict, + JSONSchema, LocalExecuteConfig, RpcExecuteConfig, ToolExecution, ToolParameters, } from './types'; + import { StackOneError } from './utils/errors'; import { TfidfIndex } from './utils/tfidf-index'; +/** + * JSON Schema with type narrowed to 'object' + * Used for tool parameter schemas which are always objects + */ +type ObjectJSONSchema = OverrideProperties; + /** * Base class for all tools. Provides common functionality for executing API calls * and converting to various formats (OpenAI, AI SDK) @@ -165,6 +174,18 @@ export class BaseTool { } } + /** + * Convert the tool parameters to a pure JSON Schema format + * This is framework-agnostic and can be used with any LLM that accepts JSON Schema + */ + toJsonSchema(): ObjectJSONSchema { + return { + type: 'object', + properties: this.parameters.properties, + required: this.parameters.required, + }; + } + /** * Convert the tool to OpenAI Chat Completions API format */ @@ -174,11 +195,7 @@ export class BaseTool { function: { name: this.name, description: this.description, - parameters: { - type: 'object', - properties: this.parameters.properties, - required: this.parameters.required, - }, + parameters: this.toJsonSchema(), }, }; } @@ -191,11 +208,7 @@ export class BaseTool { return { name: this.name, description: this.description, - input_schema: { - type: 'object', - properties: this.parameters.properties, - required: this.parameters.required, - }, + input_schema: this.toJsonSchema(), }; } @@ -211,9 +224,7 @@ export class BaseTool { description: this.description, strict, parameters: { - type: 'object', - properties: this.parameters.properties, - required: this.parameters.required, + ...this.toJsonSchema(), ...(strict ? { additionalProperties: false } : {}), }, }; @@ -389,6 +400,18 @@ export class Tools implements Iterable { return this.tools.filter((tool): tool is StackOneTool => tool instanceof StackOneTool); } + /** + * Convert all tools to pure JSON Schema format + * Returns an array of objects with name, description, and schema + */ + toJsonSchema(): Array<{ name: string; description: string; parameters: JSONSchema }> { + return this.tools.map((tool) => ({ + name: tool.name, + description: tool.description, + parameters: tool.toJsonSchema(), + })); + } + /** * Convert all tools to OpenAI Chat Completions API format */ diff --git a/src/types.ts b/src/types.ts index 0645de4..4c256d5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,7 +4,6 @@ import type { Tool } from '@ai-sdk/provider-utils'; import type { ToolSet } from 'ai'; -import type { JSONSchema7, JSONSchema7Definition } from 'json-schema'; import type { ValueOf } from 'type-fest'; /** @@ -17,15 +16,60 @@ export type JsonDict = Record; */ type Headers = Record; +/** + * JSON Schema type for defining tool input/output schemas as raw JSON Schema objects. + * This allows tools to be defined without Zod when you have JSON Schema definitions available. + */ +export interface JSONSchema { + type?: string | Array; + properties?: Record; + items?: JSONSchema | Array; + required?: Array; + enum?: Array; + const?: unknown; + description?: string; + default?: unknown; + $ref?: string; + $defs?: Record; + definitions?: Record; + allOf?: Array; + anyOf?: Array; + oneOf?: Array; + not?: JSONSchema; + if?: JSONSchema; + then?: JSONSchema; + else?: JSONSchema; + minimum?: number; + maximum?: number; + exclusiveMinimum?: number; + exclusiveMaximum?: number; + minLength?: number; + maxLength?: number; + pattern?: string; + format?: string; + minItems?: number; + maxItems?: number; + uniqueItems?: boolean; + additionalProperties?: boolean | JSONSchema; + additionalItems?: boolean | JSONSchema; + patternProperties?: Record; + propertyNames?: JSONSchema; + minProperties?: number; + maxProperties?: number; + title?: string; + examples?: Array; + [key: string]: unknown; // Allow additional properties for extensibility +} + /** * JSON Schema properties type */ -export type JsonSchemaProperties = Record; +export type JsonSchemaProperties = Record; /** - * JSON Schema type + * JSON Schema type union */ -type JsonSchemaType = JSONSchema7['type']; +type JsonSchemaType = JSONSchema['type']; /** * EXPERIMENTAL: Function to override the tool schema at creation time From ddd849526db2783a3d0c6dc4d4c58cc2e245c722 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:03:30 +0000 Subject: [PATCH 06/24] refactor(tool): use toJsonSchema() in toAISDK method Consolidate schema generation by reusing toJsonSchema() instead of manually constructing the schema object. This reduces duplication and ensures consistency across all conversion methods. --- src/tool.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/tool.ts b/src/tool.ts index 03b4fbf..aab6638 100644 --- a/src/tool.ts +++ b/src/tool.ts @@ -239,9 +239,7 @@ export class BaseTool { }, ): Promise { const schema = { - type: 'object' as const, - properties: this.parameters.properties || {}, - required: this.parameters.required || [], + ...this.toJsonSchema(), additionalProperties: false, }; From 5e9caa2ec9ee579947531f0b361af077dd1ad121 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:07:25 +0000 Subject: [PATCH 07/24] refactor(tool): add type-safe schema validation for AI SDK - Import JSONSchema7 type from @ai-sdk/provider as AISDKJSONSchema - Use satisfies AISDKJSONSchema to validate schema at compile time - Move jsonSchema type import to top-level for cleaner code - Add @ai-sdk/provider as dev dependency for type checking --- package.json | 1 + pnpm-lock.yaml | 57 +++++++++++++++++++++++++-------------------- pnpm-workspace.yaml | 5 +++- src/tool.ts | 10 ++++---- 4 files changed, 43 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index e2e5147..ce62cb3 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "zod": "catalog:dev" }, "devDependencies": { + "@ai-sdk/provider": "catalog:", "@ai-sdk/provider-utils": "catalog:dev", "@hono/mcp": "catalog:dev", "@types/node": "catalog:dev", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 07a8f3d..67bd065 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,10 @@ settings: excludeLinksFromLockfile: false catalogs: + default: + '@ai-sdk/provider': + specifier: ^2.0.0 + version: 2.0.0 dev: '@ai-sdk/openai': specifier: ^2.0.80 @@ -106,6 +110,9 @@ importers: specifier: catalog:dev version: 4.1.13 devDependencies: + '@ai-sdk/provider': + specifier: 'catalog:' + version: 2.0.0 '@ai-sdk/provider-utils': specifier: catalog:dev version: 3.0.18(zod@4.1.13) @@ -138,7 +145,7 @@ importers: version: 2.12.3(@types/node@22.19.1)(typescript@5.9.3) node: specifier: runtime:^24.11.0 - version: runtime:24.11.1 + version: runtime:24.12.0 openai: specifier: catalog:peer version: 6.9.1(zod@4.1.13) @@ -190,7 +197,7 @@ importers: version: 5.0.108(zod@4.1.13) node: specifier: runtime:^24.11.0 - version: runtime:24.11.1 + version: runtime:24.12.0 openai: specifier: catalog:peer version: 6.9.1(zod@4.1.13) @@ -2771,94 +2778,94 @@ packages: } engines: { node: '>= 0.6' } - node@runtime:24.11.1: + node@runtime:24.12.0: resolution: type: variations variants: - resolution: archive: tarball bin: bin/node - integrity: sha256-mLqRmgOQ2MQi1LsxBexbd3I89UFDtMHkudd2kPoHUgY= + integrity: sha256-MfPQZbuE8GnlIuVdDTA1vTZMul2KIiM/9oAF0oHou3g= type: binary - url: https://nodejs.org/download/release/v24.11.1/node-v24.11.1-aix-ppc64.tar.gz + url: https://nodejs.org/download/release/v24.12.0/node-v24.12.0-aix-ppc64.tar.gz targets: - cpu: ppc64 os: aix - resolution: archive: tarball bin: bin/node - integrity: sha256-sFqjpm7+aAAj+TC9WvP9u9VCeU2lZEyirXEdaMvU3DU= + integrity: sha256-MZ8iGtxeRP8O1X6KRBsihPArjcb8h7jrkqapNkP9gIA= type: binary - url: https://nodejs.org/download/release/v24.11.1/node-v24.11.1-darwin-arm64.tar.gz + url: https://nodejs.org/download/release/v24.12.0/node-v24.12.0-darwin-arm64.tar.gz targets: - cpu: arm64 os: darwin - resolution: archive: tarball bin: bin/node - integrity: sha256-CWCBttb83T9boPXx1EpH6DA3rS546tomZxwlL+ZN0RE= + integrity: sha256-uC6kxi/QjiUMq1nWJeddd8xbCj1gxmmOvuRUXIihacU= type: binary - url: https://nodejs.org/download/release/v24.11.1/node-v24.11.1-darwin-x64.tar.gz + url: https://nodejs.org/download/release/v24.12.0/node-v24.12.0-darwin-x64.tar.gz targets: - cpu: x64 os: darwin - resolution: archive: tarball bin: bin/node - integrity: sha256-Dck+xceYsNNH8GjbbSBdA96ppxdl5qU5IraCuRJl1x8= + integrity: sha256-myou65io6zc2EiTiodBgMArS3RQ69Y39sW3nhd8PEig= type: binary - url: https://nodejs.org/download/release/v24.11.1/node-v24.11.1-linux-arm64.tar.gz + url: https://nodejs.org/download/release/v24.12.0/node-v24.12.0-linux-arm64.tar.gz targets: - cpu: arm64 os: linux - resolution: archive: tarball bin: bin/node - integrity: sha256-zUFAfzNS3i8GbqJsXF0OqbY2I3TWthg4Wp8una0iBhY= + integrity: sha256-Zux5tNZPQQmu34IhCHFdC2CXEY35FZwvYyFHfaTqF6o= type: binary - url: https://nodejs.org/download/release/v24.11.1/node-v24.11.1-linux-ppc64le.tar.gz + url: https://nodejs.org/download/release/v24.12.0/node-v24.12.0-linux-ppc64le.tar.gz targets: - cpu: ppc64le os: linux - resolution: archive: tarball bin: bin/node - integrity: sha256-XUyLyl+PJZP5CB3uOYNHYOhaFvphyVDz6G7IWZbwBVA= + integrity: sha256-jclgolVdsap3/RMcJb5XG594RLyLJ454cyufWA/n1YA= type: binary - url: https://nodejs.org/download/release/v24.11.1/node-v24.11.1-linux-s390x.tar.gz + url: https://nodejs.org/download/release/v24.12.0/node-v24.12.0-linux-s390x.tar.gz targets: - cpu: s390x os: linux - resolution: archive: tarball bin: bin/node - integrity: sha256-WKX/XMjyIA5Fi+oi4ynVwZlKobER1JnKRuwkEdWCOco= + integrity: sha256-YVkifgr318PGuy+pAEUrBKbLiEGnAqeazGEyCdcLBNA= type: binary - url: https://nodejs.org/download/release/v24.11.1/node-v24.11.1-linux-x64.tar.gz + url: https://nodejs.org/download/release/v24.12.0/node-v24.12.0-linux-x64.tar.gz targets: - cpu: x64 os: linux - resolution: archive: zip bin: node.exe - integrity: sha256-zp7k5Ufr3/NVvrSOMJsWbCTfa+ApHJ6vEDzhXz3p5bQ= - prefix: node-v24.11.1-win-arm64 + integrity: sha256-sF5+Bm+BPTWtPNnCTu2u4HTAEqx+AAcSl2CP3S6UiuM= + prefix: node-v24.12.0-win-arm64 type: binary - url: https://nodejs.org/download/release/v24.11.1/node-v24.11.1-win-arm64.zip + url: https://nodejs.org/download/release/v24.12.0/node-v24.12.0-win-arm64.zip targets: - cpu: arm64 os: win32 - resolution: archive: zip bin: node.exe - integrity: sha256-U1WubXxJ7dz959NKw0hoIGAKgxv4HcO9ylyNtqm7DnY= - prefix: node-v24.11.1-win-x64 + integrity: sha256-nBJfYa6Ue1LneQlYMPnKwmeEagQ+9xkhg8hAFqqtKBI= + prefix: node-v24.12.0-win-x64 type: binary - url: https://nodejs.org/download/release/v24.11.1/node-v24.11.1-win-x64.zip + url: https://nodejs.org/download/release/v24.12.0/node-v24.12.0-win-x64.zip targets: - cpu: x64 os: win32 - version: 24.11.1 + version: 24.12.0 hasBin: true object-assign@4.1.1: @@ -4981,7 +4988,7 @@ snapshots: negotiator@1.0.0: {} - node@runtime:24.11.1: {} + node@runtime:24.12.0: {} object-assign@4.1.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 49c0e40..cdb88da 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,13 +2,16 @@ packages: - . - examples +catalog: + '@ai-sdk/provider': ^2.0.0 + catalogMode: strict catalogs: dev: '@ai-sdk/openai': ^2.0.80 - '@clack/prompts': ^0.11.0 '@ai-sdk/provider-utils': ^3.0.18 + '@clack/prompts': ^0.11.0 '@hono/mcp': ^0.1.4 '@types/node': ^22.13.5 '@typescript/native-preview': ^7.0.0-dev.20251209.1 diff --git a/src/tool.ts b/src/tool.ts index aab6638..6cd7ef8 100644 --- a/src/tool.ts +++ b/src/tool.ts @@ -1,3 +1,5 @@ +import type { jsonSchema } from 'ai'; +import type { JSONSchema7 as AISDKJSONSchema } from '@ai-sdk/provider'; import type { Tool as AnthropicTool } from '@anthropic-ai/sdk/resources'; import * as orama from '@orama/orama'; import type { ChatCompletionFunctionTool } from 'openai/resources/chat/completions'; @@ -241,20 +243,20 @@ export class BaseTool { const schema = { ...this.toJsonSchema(), additionalProperties: false, - }; + } satisfies AISDKJSONSchema; /** AI SDK is optional dependency, import only when needed */ - let jsonSchema: typeof import('ai').jsonSchema; + let jsonSchemaFn: typeof jsonSchema; try { const ai = await import('ai'); - jsonSchema = ai.jsonSchema; + jsonSchemaFn = ai.jsonSchema; } catch { throw new StackOneError( 'AI SDK is not installed. Please install it with: npm install ai@4.x|5.x or pnpm add ai@4.x|5.x', ); } - const schemaObject = jsonSchema(schema); + const schemaObject = jsonSchemaFn(schema); // TODO: Remove ts-ignore once AISDKToolDefinition properly types the inputSchema and parameters // We avoid defining our own types as much as possible, so we use the AI SDK Tool type // but need to suppress errors for backward compatibility properties From 3ce894951a1884079afc1fc1811f678a927bf23b Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:10:51 +0000 Subject: [PATCH 08/24] refactor(utils): add tryImport utility for optional dependencies - Create tryImport() helper function for dynamic imports with friendly error messages when optional dependencies are not installed - Refactor toAISDK() to use tryImport() for cleaner code - Remove unused jsonSchema type import from top-level --- src/tool.ts | 18 ++++++------------ src/utils/try-import.ts | 25 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 12 deletions(-) create mode 100644 src/utils/try-import.ts diff --git a/src/tool.ts b/src/tool.ts index 6cd7ef8..7c85e12 100644 --- a/src/tool.ts +++ b/src/tool.ts @@ -1,4 +1,3 @@ -import type { jsonSchema } from 'ai'; import type { JSONSchema7 as AISDKJSONSchema } from '@ai-sdk/provider'; import type { Tool as AnthropicTool } from '@anthropic-ai/sdk/resources'; import * as orama from '@orama/orama'; @@ -25,6 +24,7 @@ import type { import { StackOneError } from './utils/errors'; import { TfidfIndex } from './utils/tfidf-index'; +import { tryImport } from './utils/try-import'; /** * JSON Schema with type narrowed to 'object' @@ -246,17 +246,11 @@ export class BaseTool { } satisfies AISDKJSONSchema; /** AI SDK is optional dependency, import only when needed */ - let jsonSchemaFn: typeof jsonSchema; - try { - const ai = await import('ai'); - jsonSchemaFn = ai.jsonSchema; - } catch { - throw new StackOneError( - 'AI SDK is not installed. Please install it with: npm install ai@4.x|5.x or pnpm add ai@4.x|5.x', - ); - } - - const schemaObject = jsonSchemaFn(schema); + const ai = await tryImport( + 'ai', + 'npm install ai@4.x|5.x or pnpm add ai@4.x|5.x', + ); + const schemaObject = ai.jsonSchema(schema); // TODO: Remove ts-ignore once AISDKToolDefinition properly types the inputSchema and parameters // We avoid defining our own types as much as possible, so we use the AI SDK Tool type // but need to suppress errors for backward compatibility properties diff --git a/src/utils/try-import.ts b/src/utils/try-import.ts new file mode 100644 index 0000000..338c32d --- /dev/null +++ b/src/utils/try-import.ts @@ -0,0 +1,25 @@ +import { StackOneError } from './errors'; + +/** + * Dynamically import an optional dependency with a friendly error message + * + * @param moduleName - The name of the module to import + * @param installHint - Installation instructions shown in error message + * @returns The imported module + * @throws StackOneError if the module is not installed + * + * @example + * ```ts + * const ai = await tryImport('ai', 'npm install ai@4.x|5.x'); + * const { jsonSchema } = ai; + * ``` + */ +export async function tryImport(moduleName: string, installHint: string): Promise { + try { + return await import(moduleName); + } catch { + throw new StackOneError( + `${moduleName} is not installed. Please install it with: ${installHint}`, + ); + } +} From 4be98cfcff7c2e590e3944eed2256f9f97ab7fb8 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:11:34 +0000 Subject: [PATCH 09/24] test(utils): add tests for tryImport utility - Test successful import of existing modules - Test StackOneError is thrown for non-existent modules - Verify error message includes module name and install hint --- src/utils/try-import.test.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/utils/try-import.test.ts diff --git a/src/utils/try-import.test.ts b/src/utils/try-import.test.ts new file mode 100644 index 0000000..1bdbbe0 --- /dev/null +++ b/src/utils/try-import.test.ts @@ -0,0 +1,23 @@ +import { StackOneError } from './errors'; +import { tryImport } from './try-import'; + +describe('tryImport', () => { + it('should successfully import an existing module', async () => { + const result = await tryImport('node:path', 'n/a'); + expect(result).toHaveProperty('join'); + expect(typeof result.join).toBe('function'); + }); + + it('should throw StackOneError for non-existent module', async () => { + await expect( + tryImport('non-existent-module-xyz', 'npm install non-existent-module-xyz'), + ).rejects.toThrow(StackOneError); + }); + + it('should include module name and install hint in error message', async () => { + const installHint = 'npm install my-package or pnpm add my-package'; + await expect(tryImport('non-existent-module-xyz', installHint)).rejects.toThrow( + 'non-existent-module-xyz is not installed. Please install it with: npm install my-package or pnpm add my-package', + ); + }); +}); From 9d176dcdfb08daf38c57ce834cae1922db97c0c5 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:13:26 +0000 Subject: [PATCH 10/24] refactor(tool): clean up toAISDK method - Remove deprecated v4 parameters property - Use satisfies for type-safe tool definition - Remove outdated TODO comment about ts-ignore - Simplify tool definition by constructing all properties upfront --- src/tool.ts | 36 +++++++++++++++--------------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/src/tool.ts b/src/tool.ts index 7c85e12..2264a47 100644 --- a/src/tool.ts +++ b/src/tool.ts @@ -251,14 +251,6 @@ export class BaseTool { 'npm install ai@4.x|5.x or pnpm add ai@4.x|5.x', ); const schemaObject = ai.jsonSchema(schema); - // TODO: Remove ts-ignore once AISDKToolDefinition properly types the inputSchema and parameters - // We avoid defining our own types as much as possible, so we use the AI SDK Tool type - // but need to suppress errors for backward compatibility properties - const toolDefinition = { - inputSchema: schemaObject, - parameters: schemaObject, // v4 (backward compatibility) - description: this.description, - } as AISDKToolDefinition; const executionOption = options.execution !== undefined @@ -267,19 +259,21 @@ export class BaseTool { ? this.createExecutionMetadata() : false; - if (executionOption !== false) { - toolDefinition.execution = executionOption; - } - - if (options.executable ?? true) { - toolDefinition.execute = async (args: Record) => { - try { - return await this.execute(args as JsonDict); - } catch (error) { - return `Error executing tool: ${error instanceof Error ? error.message : String(error)}`; - } - }; - } + const toolDefinition = { + inputSchema: schemaObject, + description: this.description, + execution: executionOption !== false ? executionOption : undefined, + execute: + (options.executable ?? true) + ? async (args: Record) => { + try { + return await this.execute(args as JsonDict); + } catch (error) { + return `Error executing tool: ${error instanceof Error ? error.message : String(error)}`; + } + } + : undefined, + } satisfies AISDKToolDefinition; return { [this.name]: toolDefinition, From 2626c85d61519d14135e3841140bacd815fa0acd Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:14:57 +0000 Subject: [PATCH 11/24] chore(oxfmt): ignore .claude/settings.local.json --- .oxfmtrc.jsonc | 1 + src/tool.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.oxfmtrc.jsonc b/.oxfmtrc.jsonc index 758949e..8219399 100644 --- a/.oxfmtrc.jsonc +++ b/.oxfmtrc.jsonc @@ -3,4 +3,5 @@ "useTabs": true, "semi": true, "singleQuote": true, + "ignorePatterns": [".claude/settings.local.json"], } diff --git a/src/tool.ts b/src/tool.ts index 2264a47..1387f48 100644 --- a/src/tool.ts +++ b/src/tool.ts @@ -277,7 +277,7 @@ export class BaseTool { return { [this.name]: toolDefinition, - } as AISDKToolResult; + } satisfies AISDKToolResult; } } From df0b018169727a46c2e38c82a5bfd3214fbc2113 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:17:38 +0000 Subject: [PATCH 12/24] chore(deps): move @ai-sdk/provider to catalog:dev --- package.json | 2 +- pnpm-lock.yaml | 9 ++++----- pnpm-workspace.yaml | 4 +--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index ce62cb3..40b75ca 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "zod": "catalog:dev" }, "devDependencies": { - "@ai-sdk/provider": "catalog:", + "@ai-sdk/provider": "catalog:dev", "@ai-sdk/provider-utils": "catalog:dev", "@hono/mcp": "catalog:dev", "@types/node": "catalog:dev", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 67bd065..b7cbcfb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,14 +5,13 @@ settings: excludeLinksFromLockfile: false catalogs: - default: - '@ai-sdk/provider': - specifier: ^2.0.0 - version: 2.0.0 dev: '@ai-sdk/openai': specifier: ^2.0.80 version: 2.0.80 + '@ai-sdk/provider': + specifier: ^2.0.0 + version: 2.0.0 '@ai-sdk/provider-utils': specifier: ^3.0.18 version: 3.0.18 @@ -111,7 +110,7 @@ importers: version: 4.1.13 devDependencies: '@ai-sdk/provider': - specifier: 'catalog:' + specifier: catalog:dev version: 2.0.0 '@ai-sdk/provider-utils': specifier: catalog:dev diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index cdb88da..5aa9cf9 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,14 +2,12 @@ packages: - . - examples -catalog: - '@ai-sdk/provider': ^2.0.0 - catalogMode: strict catalogs: dev: '@ai-sdk/openai': ^2.0.80 + '@ai-sdk/provider': ^2.0.0 '@ai-sdk/provider-utils': ^3.0.18 '@clack/prompts': ^0.11.0 '@hono/mcp': ^0.1.4 From 468aea6510b446489fbb9af10152537956ad5511 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:21:06 +0000 Subject: [PATCH 13/24] docs: tanstack ai jsonschema --- src/types.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/types.ts b/src/types.ts index 4c256d5..fa4a22a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -19,6 +19,8 @@ type Headers = Record; /** * JSON Schema type for defining tool input/output schemas as raw JSON Schema objects. * This allows tools to be defined without Zod when you have JSON Schema definitions available. + * + * @see https://github.com/TanStack/ai/blob/049eb8acd83e6d566c6040c0c4cb53dbe222d46a/packages/typescript/ai/src/types.ts#L5C1-L49C1 */ export interface JSONSchema { type?: string | Array; From a2509631b3789e40604a3ce75f950fd761632990 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:57:11 +0000 Subject: [PATCH 14/24] test(examples): add E2E tests for example integrations Add comprehensive E2E tests for all example files using MSW mocks: - fetch-tools.test.ts: tests tool fetching, filtering, and execution - ai-sdk-integration.test.ts: tests AI SDK with OpenAI provider - openai-integration.test.ts: tests OpenAI Chat Completions API - openai-responses-integration.test.ts: tests OpenAI Responses API These tests verify the examples work correctly with mocked API responses, ensuring documentation examples remain functional. --- examples/ai-sdk-integration.test.ts | 52 +++++++++ examples/fetch-tools.test.ts | 102 ++++++++++++++++++ examples/openai-integration.test.ts | 72 +++++++++++++ examples/openai-responses-integration.test.ts | 64 +++++++++++ 4 files changed, 290 insertions(+) create mode 100644 examples/ai-sdk-integration.test.ts create mode 100644 examples/fetch-tools.test.ts create mode 100644 examples/openai-integration.test.ts create mode 100644 examples/openai-responses-integration.test.ts diff --git a/examples/ai-sdk-integration.test.ts b/examples/ai-sdk-integration.test.ts new file mode 100644 index 0000000..79239b8 --- /dev/null +++ b/examples/ai-sdk-integration.test.ts @@ -0,0 +1,52 @@ +/** + * 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 { 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: 'https://api.stackone.com', + }); + + // 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'); + }); +}); diff --git a/examples/fetch-tools.test.ts b/examples/fetch-tools.test.ts new file mode 100644 index 0000000..23fa210 --- /dev/null +++ b/examples/fetch-tools.test.ts @@ -0,0 +1,102 @@ +/** + * 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 { 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('https://api.stackone.com/actions/rpc', async ({ request }) => { + const body: unknown = await request.json(); + assert(typeof body === 'object' && body !== null); + const { action } = body as Record; + 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: 'https://api.stackone.com', + }); + + // 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('meta_')), + ).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('meta_')) + .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); + }); +}); diff --git a/examples/openai-integration.test.ts b/examples/openai-integration.test.ts new file mode 100644 index 0000000..fb59e1f --- /dev/null +++ b/examples/openai-integration.test.ts @@ -0,0 +1,72 @@ +/** + * 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 { 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: 'https://api.stackone.com', + }); + + // 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'); + + // Initialise 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'); + }); +}); diff --git a/examples/openai-responses-integration.test.ts b/examples/openai-responses-integration.test.ts new file mode 100644 index 0000000..77269d5 --- /dev/null +++ b/examples/openai-responses-integration.test.ts @@ -0,0 +1,64 @@ +/** + * 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 { 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', + }); + + // 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); + + // Initialise 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'); + }); +}); From fa898bc3076bbda8686dcaff3850141340ddbcc4 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:57:22 +0000 Subject: [PATCH 15/24] refactor(mocks): add MSW handlers for examples E2E tests - Add exampleBamboohrTools with list, get, and create employee tools - Register example account IDs in MCP handlers - Add OpenAI Responses API handler for employee lookup pattern - Fix extractTextFromInput to handle plain string input These mock handlers support the new E2E tests for example files. --- mocks/handlers.mcp.ts | 11 +++++++++- mocks/handlers.openai.ts | 24 +++++++++++++++++++++ mocks/handlers.utils.ts | 2 ++ mocks/mcp-server.ts | 46 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 82 insertions(+), 1 deletion(-) diff --git a/mocks/handlers.mcp.ts b/mocks/handlers.mcp.ts index 1bb0541..d091ce9 100644 --- a/mocks/handlers.mcp.ts +++ b/mocks/handlers.mcp.ts @@ -1,5 +1,11 @@ import { http } from 'msw'; -import { accountMcpTools, createMcpApp, defaultMcpTools, mixedProviderTools } from './mcp-server'; +import { + accountMcpTools, + createMcpApp, + defaultMcpTools, + exampleBamboohrTools, + mixedProviderTools, +} from './mcp-server'; // Create MCP apps for testing const defaultMcpApp = createMcpApp({ @@ -10,6 +16,9 @@ const defaultMcpApp = createMcpApp({ acc3: accountMcpTools.acc3, 'test-account': accountMcpTools['test-account'], mixed: mixedProviderTools, + // For examples testing + 'your-bamboohr-account-id': exampleBamboohrTools, + 'your-stackone-account-id': exampleBamboohrTools, }, }); diff --git a/mocks/handlers.openai.ts b/mocks/handlers.openai.ts index 002391e..6f004c2 100644 --- a/mocks/handlers.openai.ts +++ b/mocks/handlers.openai.ts @@ -41,6 +41,30 @@ export const openaiHandlers = [ }); } + // For openai-responses-integration.ts + if (hasTools && userMessage.includes('c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA')) { + return HttpResponse.json({ + id: 'resp_mock_responses', + object: 'response', + created_at: Date.now(), + model: 'gpt-5', + status: 'completed', + output: [ + { + type: 'function_call', + id: 'call_mock_get', + call_id: 'call_mock_get', + name: 'bamboohr_get_employee', + arguments: JSON.stringify({ + id: 'c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA', + }), + status: 'completed', + }, + ], + usage: { input_tokens: 100, output_tokens: 50, total_tokens: 150 }, + }); + } + // For human-in-the-loop.ts if (hasTools && userMessage.includes('Create a new employee')) { return HttpResponse.json({ diff --git a/mocks/handlers.utils.ts b/mocks/handlers.utils.ts index e1b8263..da47386 100644 --- a/mocks/handlers.utils.ts +++ b/mocks/handlers.utils.ts @@ -2,6 +2,8 @@ * Helper to extract text content from OpenAI responses API input */ export const extractTextFromInput = (input: unknown): string => { + // Handle string input directly (OpenAI Responses API can accept plain strings) + if (typeof input === 'string') return input; if (!Array.isArray(input)) return ''; for (const item of input) { if (typeof item === 'object' && item !== null) { diff --git a/mocks/mcp-server.ts b/mocks/mcp-server.ts index 6c7d62e..24a5325 100644 --- a/mocks/mcp-server.ts +++ b/mocks/mcp-server.ts @@ -183,6 +183,52 @@ export const accountMcpTools = { ], } as const satisfies Record; +/** Tools for the quickstart and example tests */ +export const exampleBamboohrTools = [ + { + name: 'bamboohr_list_employees', + description: 'List all employees from BambooHR', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'object', + properties: { + limit: { type: 'number', description: 'Limit the number of results' }, + }, + }, + }, + }, + }, + { + name: 'bamboohr_get_employee', + description: 'Get a single employee by ID from BambooHR', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'The employee ID' }, + fields: { type: 'string', description: 'Fields to retrieve' }, + }, + required: ['id'], + }, + }, + { + name: 'bamboohr_create_employee', + description: 'Create a new employee in BambooHR', + inputSchema: { + type: 'object', + properties: { + name: { type: 'string', description: 'Employee name' }, + personal_email: { type: 'string', description: 'Employee email' }, + department: { type: 'string', description: 'Department name' }, + start_date: { type: 'string', description: 'Start date' }, + hire_date: { type: 'string', description: 'Hire date' }, + }, + required: ['name'], + }, + }, +] as const satisfies McpToolDefinition[]; + export const mixedProviderTools = [ { name: 'hibob_list_employees', From fb802c39b03738ecf360f60362e8334123dd93bb Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:57:33 +0000 Subject: [PATCH 16/24] refactor(examples): reorganise dependencies and remove redundant index.ts - Move runtime dependencies (openai, ai, @ai-sdk/openai) to dependencies - Add msw to devDependencies for testing - Remove unused zod dependency - Add vitest/globals types for test file support - Delete redundant index.ts (duplicates fetch-tools.ts functionality) The dependency reorganisation reflects actual usage: runtime deps are used by example scripts, while msw is only needed for testing. --- examples/index.ts | 85 ------------------------------------------ examples/package.json | 12 +++--- examples/tsconfig.json | 2 +- pnpm-lock.yaml | 24 ++++++------ 4 files changed, 19 insertions(+), 104 deletions(-) delete mode 100644 examples/index.ts diff --git a/examples/index.ts b/examples/index.ts deleted file mode 100644 index 01c1bdc..0000000 --- a/examples/index.ts +++ /dev/null @@ -1,85 +0,0 @@ -/** - * # Installation - * - * ```bash - * # Using npm - * npm install @stackone/ai - * - * # Using yarn - * yarn add @stackone/ai - * - * # Using pnpm - * pnpm add @stackone/ai - * ``` - * - * # Authentication - * - * Set the `STACKONE_API_KEY` environment variable: - * - * ```bash - * export STACKONE_API_KEY= - * ``` - * - * or load from a .env file: - */ - -/** - * # Account IDs - * - * StackOne uses account IDs to identify different integrations. - * Replace the placeholder below with your actual account ID from the StackOne dashboard. - */ - -import process from 'node:process'; - -// Replace with your actual account ID from StackOne dashboard -const accountId = 'your-bamboohr-account-id'; - -/** - * # Quickstart - */ - -import assert from 'node:assert'; -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 quickstart = async (): Promise => { - const toolset = new StackOneToolSet({ - accountId, - baseUrl: process.env.STACKONE_BASE_URL ?? 'https://api.stackone.com', - }); - - // Fetch all tools for this account via MCP - const tools = await toolset.fetchTools(); - - // Verify we have tools - assert(tools.length > 0, 'Expected to find tools'); - - // Use a specific tool - const employeeTool = tools.getTool('bamboohr_list_employees'); - assert(employeeTool !== undefined, 'Expected to find bamboohr_list_employees tool'); - - // Execute the tool and verify the response - const result = await employeeTool.execute(); - assert(Array.isArray(result.data), 'Expected employees to be an array'); - assert(result.data.length > 0, 'Expected to find at least one employee'); -}; - -// Run the example -await quickstart(); - -/** - * # Next Steps - * - * Check out some more examples: - * - * - [OpenAI Integration](openai-integration.md) - * - [AI SDK Integration](ai-sdk-integration.md) - * - [Fetch Tools](fetch-tools.md) - * - [Meta Tools](meta-tools.md) - */ diff --git a/examples/package.json b/examples/package.json index 17919d4..1e90c2e 100644 --- a/examples/package.json +++ b/examples/package.json @@ -8,15 +8,15 @@ "format": "oxlint --max-warnings=0 --type-aware --type-check --fix" }, "dependencies": { - "@stackone/ai": "workspace:*" - }, - "devDependencies": { "@ai-sdk/openai": "catalog:dev", "@clack/prompts": "catalog:dev", - "@types/node": "catalog:dev", + "@stackone/ai": "workspace:*", "ai": "catalog:peer", - "openai": "catalog:peer", - "zod": "catalog:dev" + "openai": "catalog:peer" + }, + "devDependencies": { + "@types/node": "catalog:dev", + "msw": "catalog:dev" }, "devEngines": { "runtime": [ diff --git a/examples/tsconfig.json b/examples/tsconfig.json index 2f1f9a6..72ce66a 100644 --- a/examples/tsconfig.json +++ b/examples/tsconfig.json @@ -9,7 +9,7 @@ "module": "ESNext", "moduleResolution": "bundler", "lib": ["es2022"], - "types": ["node"], + "types": ["node", "vitest/globals"], }, "include": ["**/*.ts"], "exclude": ["node_modules"], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 73192fb..6646976 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -175,31 +175,31 @@ importers: examples: dependencies: - '@stackone/ai': - specifier: workspace:* - version: link:.. - devDependencies: '@ai-sdk/openai': specifier: catalog:dev version: 2.0.80(zod@4.1.13) '@clack/prompts': specifier: catalog:dev version: 0.11.0 - '@types/node': - specifier: catalog:dev - version: 22.19.1 + '@stackone/ai': + specifier: workspace:* + version: link:.. ai: specifier: catalog:peer version: 5.0.108(zod@4.1.13) - node: - specifier: runtime:^24.11.0 - version: runtime:24.11.1 openai: specifier: catalog:peer version: 6.9.1(zod@4.1.13) - zod: + devDependencies: + '@types/node': specifier: catalog:dev - version: 4.1.13 + version: 22.19.1 + msw: + specifier: catalog:dev + version: 2.12.3(@types/node@22.19.1)(typescript@5.9.3) + node: + specifier: runtime:^24.11.0 + version: runtime:24.11.1 packages: '@ai-sdk/gateway@2.0.18': From d541b0b7710e9667b012e8284e47df02d49117bd Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:57:47 +0000 Subject: [PATCH 17/24] refactor(vitest): consolidate setupFiles and simplify coverage config - Move setupFiles to root level (inherited via extends: true) - Limit coverage to src/**/*.ts only (examples are documentation) - Remove redundant per-project setupFiles configuration The setupFiles change reduces duplication since both root and examples projects use the same MSW setup. Coverage excludes examples since they are documentation files, not library code. --- vitest.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vitest.config.ts b/vitest.config.ts index 69d2bb2..7177643 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -5,10 +5,11 @@ export default defineConfig({ watch: false, globals: true, testTimeout: 30000, + setupFiles: ['./vitest.setup.ts'], coverage: { provider: 'v8', reporter: ['text', 'json', 'json-summary', 'html'], - include: ['src/**/*.ts', 'examples/**/*.ts'], + include: ['src/**/*.ts'], exclude: ['**/*.test.ts', '**/*.test-d.ts'], }, projects: [ @@ -19,7 +20,6 @@ export default defineConfig({ root: '.', include: ['src/**/*.test.ts', 'scripts/**/*.test.ts'], exclude: ['node_modules', 'dist', 'examples'], - setupFiles: ['./vitest.setup.ts'], typecheck: { enabled: true, include: ['src/**/*.test.ts', 'src/**/*.test-d.ts'], From 3b395df7f2a0e1b3440ce84cf3f5996cad311919 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:57:58 +0000 Subject: [PATCH 18/24] chore: update knip config and package.json - Add *.test.ts to examples entry points in knip config - Remove lefthook from ignoreDependencies (no longer needed) - Add examples/*.ts to package.json files for distribution - Exclude example test files from distribution --- knip.config.ts | 4 ++-- package.json | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/knip.config.ts b/knip.config.ts index 5008c57..9963d45 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -7,13 +7,13 @@ export default { project: ['src/**/*.ts', 'mocks/**/*.ts'], }, examples: { - entry: ['*.ts'], + entry: ['*.ts', '*.test.ts'], project: ['*.ts'], }, }, ignore: ['**/*.test.ts', '**/*.spec.ts', '**/*.test-d.ts'], ignoreBinaries: ['only-allow'], - ignoreDependencies: ['@typescript/native-preview', 'lefthook'], + ignoreDependencies: ['@typescript/native-preview'], rules: { optionalPeerDependencies: 'off', devDependencies: 'warn', diff --git a/package.json b/package.json index 8079f9a..586cf4f 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,9 @@ "src", "dist", "README.md", - "LICENSE" + "LICENSE", + "examples/*.ts", + "!example/*.test.ts" ], "scripts": { "build": "tsdown", From db3d684ffa2549b51d3f2de2246d4747150797ae Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Fri, 12 Dec 2025 14:20:27 +0000 Subject: [PATCH 19/24] feat(deps): add TanStack AI and Claude Agent SDK to examples catalog - Create new 'examples' catalog for example-specific dependencies - Add @tanstack/ai and @tanstack/ai-openai for TanStack AI integration - Add @anthropic-ai/claude-agent-sdk for Claude Agent SDK integration - Configure minimumReleaseAgeExclude for newly released packages - Update examples/package.json to use catalog:examples references --- examples/package.json | 7 +- package.json | 2 - pnpm-lock.yaml | 300 ++++++++++++++++++++++++++++++++++++++++++ pnpm-workspace.yaml | 9 ++ 4 files changed, 312 insertions(+), 6 deletions(-) diff --git a/examples/package.json b/examples/package.json index c778c4b..f26ee54 100644 --- a/examples/package.json +++ b/examples/package.json @@ -3,15 +3,14 @@ "version": "0.0.0", "private": true, "type": "module", - "scripts": { - "lint": "oxlint --max-warnings=0 --type-aware --type-check", - "format": "oxlint --max-warnings=0 --type-aware --type-check --fix" - }, "dependencies": { "@ai-sdk/openai": "catalog:dev", + "@anthropic-ai/claude-agent-sdk": "catalog:examples", "@anthropic-ai/sdk": "catalog:peer", "@clack/prompts": "catalog:dev", "@stackone/ai": "workspace:*", + "@tanstack/ai": "catalog:examples", + "@tanstack/ai-openai": "catalog:examples", "ai": "catalog:peer", "openai": "catalog:peer" }, diff --git a/package.json b/package.json index 8bd7aea..99464bb 100644 --- a/package.json +++ b/package.json @@ -25,11 +25,9 @@ "format:oxfmt": "oxfmt --no-error-on-unmatched-pattern .", "format:oxlint": "oxlint --max-warnings=0 --type-aware --type-check --fix", "format:knip": "knip --fix --no-exit-code", - "format:submodule": "pnpm --parallel -r --aggregate-output format", "lint": "pnpm --aggregate-output run '/^lint:/'", "lint:oxfmt": "oxfmt --no-error-on-unmatched-pattern --check .", "lint:oxlint": "oxlint --max-warnings=0 --type-aware --type-check", - "lint:submodule": "pnpm --parallel -r --aggregate-output lint", "lint:knip": "knip", "preinstall": "npx only-allow pnpm", "prepack": "npm pkg delete scripts.preinstall && pnpm run build", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index feaa818..afdd528 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,6 +69,16 @@ catalogs: zod: specifier: ^4.1.13 version: 4.1.13 + examples: + '@anthropic-ai/claude-agent-sdk': + specifier: ^0.1.67 + version: 0.1.67 + '@tanstack/ai': + specifier: ^0.0.3 + version: 0.0.3 + '@tanstack/ai-openai': + specifier: ^0.0.3 + version: 0.0.3 peer: '@anthropic-ai/sdk': specifier: ^0.52.0 @@ -178,6 +188,9 @@ importers: '@ai-sdk/openai': specifier: catalog:dev version: 2.0.80(zod@4.1.13) + '@anthropic-ai/claude-agent-sdk': + specifier: catalog:examples + version: 0.1.67(zod@4.1.13) '@anthropic-ai/sdk': specifier: catalog:peer version: 0.52.0 @@ -187,6 +200,12 @@ importers: '@stackone/ai': specifier: workspace:* version: link:.. + '@tanstack/ai': + specifier: catalog:examples + version: 0.0.3(@alcyone-labs/zod-to-json-schema@4.0.10(zod@4.1.13))(zod@4.1.13) + '@tanstack/ai-openai': + specifier: catalog:examples + version: 0.0.3(@tanstack/ai@0.0.3(@alcyone-labs/zod-to-json-schema@4.0.10(zod@4.1.13))(zod@4.1.13))(zod@4.1.13) ai: specifier: catalog:peer version: 5.0.108(zod@4.1.13) @@ -239,6 +258,23 @@ packages: } engines: { node: '>=18' } + '@alcyone-labs/zod-to-json-schema@4.0.10': + resolution: + { + integrity: sha512-TFsSpAPToqmqmT85SGHXuxoCwEeK9zUDvn512O9aBVvWRhSuy+VvAXZkifzsdllD3ncF0ZjUrf4MpBwIEixdWQ==, + } + peerDependencies: + zod: ^4.0.5 + + '@anthropic-ai/claude-agent-sdk@0.1.67': + resolution: + { + integrity: sha512-SPeMOfBeQ4Q6BcTRGRyMzaSEzKja3w8giZn6xboab02rPly5KQmgDK0wNerUntPe+xyw7c01xdu5K/pjZXq0dw==, + } + engines: { node: '>=18.0.0' } + peerDependencies: + zod: ^3.24.1 + '@anthropic-ai/sdk@0.52.0': resolution: { @@ -796,6 +832,144 @@ packages: '@modelcontextprotocol/sdk': ^1.12.0 hono: '>=4.0.0' + '@img/sharp-darwin-arm64@0.33.5': + resolution: + { + integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==, + } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.33.5': + resolution: + { + integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==, + } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: + { + integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==, + } + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: + { + integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==, + } + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: + { + integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==, + } + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: + { + integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==, + } + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: + { + integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==, + } + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: + { + integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==, + } + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: + { + integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==, + } + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.33.5': + resolution: + { + integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==, + } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.33.5': + resolution: + { + integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==, + } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.33.5': + resolution: + { + integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==, + } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: + { + integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==, + } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: + { + integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==, + } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-win32-x64@0.33.5': + resolution: + { + integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==, + } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + cpu: [x64] + os: [win32] + '@inquirer/ansi@1.0.2': resolution: { @@ -1646,6 +1820,31 @@ packages: integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==, } + '@tanstack/ai-openai@0.0.3': + resolution: + { + integrity: sha512-JyV5KMvaUIkS/9mt8zdu+8Sl0+/btbwrsreuFXftbrL8H+ysvbmFW3KwD2eUdTBwNPv2szUn5su17X1yt1CphQ==, + } + peerDependencies: + '@tanstack/ai': 0.0.3 + + '@tanstack/ai@0.0.3': + resolution: + { + integrity: sha512-zwSl0obT/fkUZocI22xClGNg66yWiRw3d3BKz7F5/V8E+JWFZk2Gh4P7V9wrr0uUEjsZlt8u7qNDgBzwU9uu9g==, + } + engines: { node: '>=18' } + peerDependencies: + '@alcyone-labs/zod-to-json-schema': ^4.0.0 + zod: ^3.0.0 || ^4.0.0 + + '@tanstack/devtools-event-client@0.4.0': + resolution: + { + integrity: sha512-RPfGuk2bDZgcu9bAJodvO2lnZeHuz4/71HjZ0bGb/SPg8+lyTA+RLSKQvo7fSmPSi8/vcH3aKQ8EM9ywf1olaw==, + } + engines: { node: '>=18' } + '@tybys/wasm-util@0.10.1': resolution: { @@ -2968,6 +3167,12 @@ packages: } engines: { node: '>= 0.8' } + partial-json@0.1.7: + resolution: + { + integrity: sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==, + } + path-key@3.1.1: resolution: { @@ -3737,6 +3942,23 @@ snapshots: dependencies: json-schema: 0.4.0 + '@alcyone-labs/zod-to-json-schema@4.0.10(zod@4.1.13)': + dependencies: + zod: 4.1.13 + + '@anthropic-ai/claude-agent-sdk@0.1.67(zod@4.1.13)': + dependencies: + zod: 4.1.13 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + '@anthropic-ai/sdk@0.52.0': {} '@babel/generator@7.28.5': @@ -3950,6 +4172,65 @@ snapshots: '@modelcontextprotocol/sdk': 1.24.3(zod@4.1.13) hono: 4.10.7 + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + + '@img/sharp-darwin-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + optional: true + + '@img/sharp-linux-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.4 + optional: true + + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + optional: true + + '@img/sharp-win32-x64@0.33.5': + optional: true + '@inquirer/ansi@1.0.2': {} '@inquirer/confirm@5.1.21(@types/node@22.19.1)': @@ -4304,6 +4585,23 @@ snapshots: '@standard-schema/spec@1.0.0': {} + '@tanstack/ai-openai@0.0.3(@tanstack/ai@0.0.3(@alcyone-labs/zod-to-json-schema@4.0.10(zod@4.1.13))(zod@4.1.13))(zod@4.1.13)': + dependencies: + '@tanstack/ai': 0.0.3(@alcyone-labs/zod-to-json-schema@4.0.10(zod@4.1.13))(zod@4.1.13) + openai: 6.9.1(zod@4.1.13) + transitivePeerDependencies: + - ws + - zod + + '@tanstack/ai@0.0.3(@alcyone-labs/zod-to-json-schema@4.0.10(zod@4.1.13))(zod@4.1.13)': + dependencies: + '@alcyone-labs/zod-to-json-schema': 4.0.10(zod@4.1.13) + '@tanstack/devtools-event-client': 0.4.0 + partial-json: 0.1.7 + zod: 4.1.13 + + '@tanstack/devtools-event-client@0.4.0': {} + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -5068,6 +5366,8 @@ snapshots: parseurl@1.3.3: {} + partial-json@0.1.7: {} + path-key@3.1.1: {} path-to-regexp@6.3.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 5aa9cf9..f13557b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -27,6 +27,10 @@ catalogs: unplugin-unused: ^0.5.4 vitest: ^4.0.15 zod: ^4.1.13 + examples: + '@anthropic-ai/claude-agent-sdk': ^0.1.67 + '@tanstack/ai': ^0.0.3 + '@tanstack/ai-openai': ^0.0.3 peer: '@anthropic-ai/sdk': ^0.52.0 ai: ^5.0.108 @@ -40,6 +44,11 @@ enablePrePostScripts: true minimumReleaseAge: 1440 +minimumReleaseAgeExclude: + - '@anthropic-ai/claude-agent-sdk' + - '@tanstack/ai' + - '@tanstack/ai-openai' + onlyBuiltDependencies: - esbuild - lefthook From 2c534f73d04c877cffc00cd665b84997a3f98591 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Fri, 12 Dec 2025 14:20:36 +0000 Subject: [PATCH 20/24] feat(examples): add TanStack AI integration example and tests - Add tanstack-ai-integration.ts demonstrating StackOne tools with TanStack AI - TanStack AI requires Zod schemas for tool input validation - Add E2E tests for tool setup and direct execution - The adapter reads OPENAI_API_KEY from environment automatically --- examples/tanstack-ai-integration.test.ts | 84 ++++++++++++++++++++++++ examples/tanstack-ai-integration.ts | 83 +++++++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 examples/tanstack-ai-integration.test.ts create mode 100644 examples/tanstack-ai-integration.ts diff --git a/examples/tanstack-ai-integration.test.ts b/examples/tanstack-ai-integration.test.ts new file mode 100644 index 0000000..17b1fe1 --- /dev/null +++ b/examples/tanstack-ai-integration.test.ts @@ -0,0 +1,84 @@ +/** + * 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 { 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: 'https://api.stackone.com', + }); + + // 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(); + + // 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: async (args: Record) => { + return employeeTool!.execute(args); + }, + }; + + expect(getEmployeeTool.name).toBe('bamboohr_get_employee'); + expect(getEmployeeTool.description).toContain('employee'); + expect(getEmployeeTool.inputSchema).toBeDefined(); + expect(getEmployeeTool.inputSchema.type).toBe('object'); + expect(typeof getEmployeeTool.execute).toBe('function'); + }); + + it('should execute tool directly', async () => { + const toolset = new StackOneToolSet({ + accountId: 'your-bamboohr-account-id', + baseUrl: 'https://api.stackone.com', + }); + + 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: async (args: Record) => { + return employeeTool.execute(args); + }, + }; + + // Execute the tool directly to verify it works + const result = await getEmployeeTool.execute({ + id: 'c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA', + }); + + expect(result).toBeDefined(); + expect(result).toHaveProperty('data'); + }); +}); diff --git a/examples/tanstack-ai-integration.ts b/examples/tanstack-ai-integration.ts new file mode 100644 index 0000000..b81e7f0 --- /dev/null +++ b/examples/tanstack-ai-integration.ts @@ -0,0 +1,83 @@ +/** + * 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 { openai } 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); +} + +// Replace with your actual account ID from StackOne dashboard +const accountId = 'your-bamboohr-account-id'; + +const tanstackAiIntegration = async (): Promise => { + // Initialise StackOne + const toolset = new StackOneToolSet({ + accountId, + baseUrl: process.env.STACKONE_BASE_URL ?? 'https://api.stackone.com', + }); + + // 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 = openai(); + const stream = chat({ + adapter, + model: 'gpt-4o', + messages: [ + { + role: 'user', + content: 'Get the employee with id: c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA', + }, + ], + tools: [getEmployeeTool], + }); + + // Process the stream + let hasToolCall = false; + for await (const chunk of stream) { + if (chunk.type === 'tool_call') { + hasToolCall = true; + assert( + chunk.toolCall.function.name === 'bamboohr_get_employee', + 'Expected tool call to be bamboohr_get_employee', + ); + } + } + + assert(hasToolCall, 'Expected at least one tool call'); +}; + +await tanstackAiIntegration(); From 9666c32ba4124ab3fc104f28759f080d92249d8d Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Fri, 12 Dec 2025 14:20:43 +0000 Subject: [PATCH 21/24] feat(examples): add Claude Agent SDK integration example and tests - Add claude-agent-sdk-integration.ts demonstrating StackOne tools with Claude Agent SDK - Create custom MCP server using createSdkMcpServer() with StackOne tools - Wrap StackOne tools using tool() function with Zod schemas - Add E2E tests for tool wrapper, MCP server creation, and direct execution - Note: Actual query execution requires ANTHROPIC_API_KEY and claude-code installation --- examples/claude-agent-sdk-integration.test.ts | 140 ++++++++++++++++++ examples/claude-agent-sdk-integration.ts | 88 +++++++++++ 2 files changed, 228 insertions(+) create mode 100644 examples/claude-agent-sdk-integration.test.ts create mode 100644 examples/claude-agent-sdk-integration.ts diff --git a/examples/claude-agent-sdk-integration.test.ts b/examples/claude-agent-sdk-integration.test.ts new file mode 100644 index 0000000..ab21408 --- /dev/null +++ b/examples/claude-agent-sdk-integration.test.ts @@ -0,0 +1,140 @@ +/** + * 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 { 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: 'https://api.stackone.com', + }); + + // 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: 'https://api.stackone.com', + }); + + 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: 'https://api.stackone.com', + }); + + 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'); + }); +}); diff --git a/examples/claude-agent-sdk-integration.ts b/examples/claude-agent-sdk-integration.ts new file mode 100644 index 0000000..6057dc2 --- /dev/null +++ b/examples/claude-agent-sdk-integration.ts @@ -0,0 +1,88 @@ +/** + * This example shows how to use StackOne tools with Claude Agent SDK. + * + * Claude Agent SDK allows you to create autonomous agents with custom tools + * via MCP (Model Context Protocol) servers. + */ + +import assert from 'node:assert'; +import process from 'node:process'; +import { query, tool, createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk'; +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); +} + +// Replace with your actual account ID from StackOne dashboard +const accountId = 'your-bamboohr-account-id'; + +const claudeAgentSdkIntegration = async (): Promise => { + // Initialise StackOne + const toolset = new StackOneToolSet({ + accountId, + baseUrl: process.env.STACKONE_BASE_URL ?? 'https://api.stackone.com', + }); + + // Fetch tools from StackOne + const tools = await toolset.fetchTools(); + + // Get a specific tool + const employeeTool = tools.getTool('bamboohr_get_employee'); + assert(employeeTool !== undefined, 'Expected to find bamboohr_get_employee tool'); + + // Create a Claude Agent SDK tool from the 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', text: JSON.stringify(result) }], + }; + }, + ); + + // Create an MCP server with the StackOne tool + const mcpServer = createSdkMcpServer({ + name: 'stackone-tools', + version: '1.0.0', + tools: [getEmployeeTool], + }); + + // Use the Claude Agent SDK query with the custom MCP server + const result = query({ + prompt: 'Get the employee with id: c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA', + options: { + model: 'claude-sonnet-4-5-20250929', + mcpServers: { + 'stackone-tools': mcpServer, + }, + // Disable built-in tools, only use our custom tools + tools: [], + maxTurns: 3, + }, + }); + + // Process the stream and collect results + let hasToolCall = false; + for await (const message of result) { + if (message.type === 'assistant') { + for (const block of message.message.content) { + if (block.type === 'tool_use' && block.name === 'bamboohr_get_employee') { + hasToolCall = true; + } + } + } + } + + assert(hasToolCall, 'Expected at least one tool call to bamboohr_get_employee'); +}; + +await claudeAgentSdkIntegration(); From c69e1878a86d714aa472e1ef0690518313af97c0 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Fri, 12 Dec 2025 14:20:50 +0000 Subject: [PATCH 22/24] docs(examples): clarify AI SDK agent pattern in comments - Document that stepCountIs() creates an agent-like multi-step tool loop - Reference AI SDK v6+ ToolLoopAgent for explicit agent functionality --- examples/ai-sdk-integration.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/examples/ai-sdk-integration.ts b/examples/ai-sdk-integration.ts index 4df068e..d39b2dc 100644 --- a/examples/ai-sdk-integration.ts +++ b/examples/ai-sdk-integration.ts @@ -1,5 +1,13 @@ /** * This example shows how to use StackOne tools with the AI SDK. + * + * The AI SDK provides an agent-like pattern through the `stopWhen` parameter + * with `stepCountIs()`. This creates a multi-step tool loop where the model + * can autonomously call tools and reason over results until the stop condition + * is met. + * + * In AI SDK v6+, you can use the `ToolLoopAgent` class for more explicit + * agent functionality. */ import assert from 'node:assert'; From ba649139ccc0dff9707493c438ef3e9af1aad470 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Fri, 12 Dec 2025 14:20:56 +0000 Subject: [PATCH 23/24] docs(readme): add TanStack AI and Claude Agent SDK integration examples - Add TanStack AI integration section with Zod schema example - Add Claude Agent SDK integration section with MCP server example - Remove obsolete link to deleted examples/index.ts --- README.md | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 94 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ab32b8a..b64a42d 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,100 @@ await generateText({ [View full example](examples/ai-sdk-integration.ts) +### TanStack AI + +```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({ + baseUrl: "https://api.stackone.com", + 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-4o", + 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) + +### Claude Agent SDK + +```typescript +import { query, tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk"; +import { z } from "zod"; +import { StackOneToolSet } from "@stackone/ai"; + +const toolset = new StackOneToolSet({ + baseUrl: "https://api.stackone.com", + accountId: "your-account-id", +}); + +const tools = await toolset.fetchTools(); +const employeeTool = tools.getTool("bamboohr_get_employee"); + +// Create a Claude Agent SDK tool from the 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", text: JSON.stringify(result) }] }; + } +); + +// Create an MCP server with the StackOne tool +const mcpServer = createSdkMcpServer({ + name: "stackone-tools", + version: "1.0.0", + tools: [getEmployeeTool], +}); + +// Use with Claude Agent SDK query +const result = query({ + prompt: "Get the employee with id: abc123", + options: { + model: "claude-sonnet-4-5-20250929", + mcpServers: { "stackone-tools": mcpServer }, + tools: [], // Disable built-in tools + maxTurns: 3, + }, +}); + +for await (const message of result) { + // Process streaming messages +} +``` + +[View full example](examples/claude-agent-sdk-integration.ts) + ## Usage ```typescript @@ -128,8 +222,6 @@ const employeeTool = tools.getTool("bamboohr_list_employees"); const employees = await employeeTool.execute(); ``` -[View full example](examples/index.ts) - ### Authentication Set the `STACKONE_API_KEY` environment variable: From 30f05071a03e3f1bc66f101a05c7c41cf129b1ad Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Fri, 12 Dec 2025 14:26:25 +0000 Subject: [PATCH 24/24] docs: --- examples/fetch-tools-debug.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/fetch-tools-debug.ts b/examples/fetch-tools-debug.ts index bd140b2..f595e67 100644 --- a/examples/fetch-tools-debug.ts +++ b/examples/fetch-tools-debug.ts @@ -11,7 +11,7 @@ * * Run with: * ```bash - * npx tsx examples/interactive-cli.ts + * node --env-files=.env examples/interactive-cli.ts * ``` */