Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 35 additions & 63 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ bun add @stackone/ai zod
import { StackOneToolSet } from '@stackone/ai';

const toolset = new StackOneToolSet({
baseUrl: 'https://api.stackone.com',
accountId: 'your-account-id',
});

Expand Down Expand Up @@ -97,7 +96,6 @@ import { OpenAI } from 'openai';
import { StackOneToolSet } from '@stackone/ai';

const toolset = new StackOneToolSet({
baseUrl: 'https://api.stackone.com',
accountId: 'your-account-id',
});

Expand Down Expand Up @@ -135,7 +133,6 @@ import OpenAI from 'openai';
import { StackOneToolSet } from '@stackone/ai';

const toolset = new StackOneToolSet({
baseUrl: 'https://api.stackone.com',
accountId: 'your-account-id',
});

Expand Down Expand Up @@ -167,7 +164,6 @@ import Anthropic from '@anthropic-ai/sdk';
import { StackOneToolSet } from '@stackone/ai';

const toolset = new StackOneToolSet({
baseUrl: 'https://api.stackone.com',
accountId: 'your-account-id',
});

Expand Down Expand Up @@ -206,7 +202,6 @@ import { generateText } from 'ai';
import { StackOneToolSet } from '@stackone/ai';

const toolset = new StackOneToolSet({
baseUrl: 'https://api.stackone.com',
accountId: 'your-account-id',
});

Expand Down Expand Up @@ -237,7 +232,6 @@ import { z } from 'zod';
import { StackOneToolSet } from '@stackone/ai';

const toolset = new StackOneToolSet({
baseUrl: 'https://api.stackone.com',
accountId: 'your-account-id',
});

Expand Down Expand Up @@ -285,7 +279,6 @@ import { query } from '@anthropic-ai/claude-agent-sdk';
import { StackOneToolSet } from '@stackone/ai';

const toolset = new StackOneToolSet({
baseUrl: 'https://api.stackone.com',
accountId: 'your-account-id',
});

Expand Down Expand Up @@ -355,77 +348,60 @@ This is especially useful when you want to:

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

### Utility Tools (Beta)
### Search Tool

Utility tools enable dynamic tool discovery and execution, allowing AI agents to search for relevant tools based on natural language queries without hardcoding tool names.

> **Beta Feature**: Utility tools are currently in beta and the API may change in future versions.

#### How Utility Tools Work

Utility tools provide two core capabilities:

1. **Tool Discovery** (`tool_search`): Search for tools using natural language queries
2. **Tool Execution** (`tool_execute`): Execute discovered tools dynamically
Search for tools using natural language queries. Works with both semantic (cloud) and local BM25+TF-IDF search.

#### Basic Usage

```typescript
import { StackOneToolSet } from '@stackone/ai';

const toolset = new StackOneToolSet({
baseUrl: 'https://api.stackone.com',
});
const tools = await toolset.fetchTools();
// Get a callable search tool
const toolset = new StackOneToolSet({ accountId: 'your-account-id' });
const searchTool = toolset.getSearchTool();

// Get utility tools for dynamic discovery
const utilityTools = await tools.utilityTools();
// Search for relevant tools — returns a Tools collection
const tools = await searchTool.search('manage employees', { topK: 5 });

// Use with OpenAI
const openAITools = utilityTools.toOpenAI();

// Use with AI SDK
const aiSdkTools = await utilityTools.toAISDK();
// Execute a discovered tool directly
const listTool = tools.getTool('bamboohr_list_employees');
const result = await listTool.execute({ query: { limit: 10 } });
```

#### Example: Dynamic Tool Discovery with AI SDK
### Semantic Search

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

```typescript
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { StackOneToolSet } from '@stackone/ai';

const { text } = await generateText({
model: openai('gpt-5.1'),
tools: aiSdkTools,
prompt: 'Find tools for managing employees and create a time off request',
maxSteps: 3, // Allow multiple tool calls
});
const toolset = new StackOneToolSet({ accountId: 'your-account-id' });

// Search by intent — returns Tools collection ready for any framework
const tools = await toolset.searchTools('manage employee records', { topK: 5 });
const openAITools = tools.toOpenAI();

// Lightweight: inspect results without fetching full tool definitions
const results = await toolset.searchActionNames('time off requests', { topK: 5 });
```

#### Direct Usage Without AI
#### Search Modes

Control which search backend `searchTools()` uses via the `search` option:

```typescript
// Step 1: Discover relevant tools
const filterTool = utilityTools.getTool('tool_search');
const searchResult = await filterTool.execute({
query: 'employee time off vacation',
limit: 5,
minScore: 0.3, // Minimum relevance score (0-1)
});
// 'auto' (default) — tries semantic search first, falls back to local
const tools = await toolset.searchTools('manage employees', { search: 'auto' });

// Step 2: Execute a discovered tool
const executeTool = utilityTools.getTool('tool_execute');
const result = await executeTool.execute({
toolName: 'bamboohr_create_time_off',
params: {
employeeId: 'emp_123',
startDate: '2024-01-15',
endDate: '2024-01-19',
},
});
// 'semantic' — semantic API only, throws if unavailable
const tools = await toolset.searchTools('manage employees', { search: 'semantic' });

// 'local' — local BM25+TF-IDF only, no semantic API call
const tools = await toolset.searchTools('manage employees', { search: 'local' });
```

[View full example](examples/utility-tools.ts)
Results are automatically scoped to connectors in your linked accounts. See [Search Tools Example](examples/search-tools.ts) for `SearchTool` (`getSearchTool`) integration, AI SDK, and agent loop patterns.

### Custom Base URL

Expand All @@ -443,9 +419,7 @@ You can use the `dryRun` option to return the api arguments from a tool call wit
import { StackOneToolSet } from '@stackone/ai';

// Initialize the toolset
const toolset = new StackOneToolSet({
baseUrl: 'https://api.stackone.com',
});
const toolset = new StackOneToolSet();

const tools = await toolset.fetchTools();
const employeeTool = tools.getTool('bamboohr_list_employees');
Expand Down Expand Up @@ -492,9 +466,7 @@ The feedback tool is automatically available when using `StackOneToolSet`:
```typescript
import { StackOneToolSet } from '@stackone/ai';

const toolset = new StackOneToolSet({
baseUrl: 'https://api.stackone.com',
});
const toolset = new StackOneToolSet();
const tools = await toolset.fetchTools();

// The feedback tool is automatically included
Expand Down
19 changes: 19 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,25 @@ Shows how to implement human-in-the-loop workflows for validation.
- **API Calls**: Conditional
- **Key Features**: Manual approval workflows, UI integration patterns

### Semantic Search

#### [`search-tools.ts`](./search-tools.ts) - Semantic Tool Search

Demonstrates dynamic tool discovery using semantic search. Includes four examples:

1. **Semantic search + AI SDK** — search for tools by natural language query, then use them with `generateText`
2. **SearchTool for agent loops** — reusable search tool for multi-step agent workflows
3. **Lightweight action name search** — search action names without fetching full tool definitions
4. **Local-only search** — BM25+TF-IDF search with no API call to the semantic search endpoint

```bash
# Run without OpenAI (examples 2-4)
npx tsx examples/search-tools.ts

# Run all 4 examples
OPENAI_API_KEY=your-key npx tsx examples/search-tools.ts
```

### OpenAPI Toolset Examples

#### [`openapi-toolset.ts`](./openapi-toolset.ts) - OpenAPI Integration
Expand Down
3 changes: 2 additions & 1 deletion examples/ai-sdk-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import { openai } from '@ai-sdk/openai';
import { generateText, stepCountIs } from 'ai';
import { TEST_BASE_URL } from '../mocks/constants';
import { StackOneToolSet } from '../src';

describe('ai-sdk-integration example e2e', () => {
Expand All @@ -21,7 +22,7 @@ describe('ai-sdk-integration example e2e', () => {
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',
baseUrl: TEST_BASE_URL,
});

// Fetch all tools for this account via MCP
Expand Down
10 changes: 2 additions & 8 deletions examples/ai-sdk-integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,9 @@ if (!apiKey) {
process.exit(1);
}

// Replace with your actual account ID from StackOne dashboard
const accountId = 'your-bamboohr-account-id';

const aiSdkIntegration = async (): Promise<void> => {
// Initialize StackOne
const toolset = new StackOneToolSet({
accountId,
baseUrl: process.env.STACKONE_BASE_URL ?? 'https://api.stackone.com',
});
// Initialize StackOne — reads STACKONE_API_KEY and STACKONE_ACCOUNT_ID from env
const toolset = new StackOneToolSet();

// Fetch all tools for this account via MCP
const tools = await toolset.fetchTools();
Expand Down
10 changes: 2 additions & 8 deletions examples/anthropic-integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,9 @@ if (!apiKey) {
process.exit(1);
}

// Replace with your actual account ID from StackOne dashboard
const accountId = 'your-hris-account-id';

const anthropicIntegration = async (): Promise<void> => {
// Initialize StackOne
const toolset = new StackOneToolSet({
accountId,
baseUrl: process.env.STACKONE_BASE_URL ?? 'https://api.stackone.com',
});
// Initialize StackOne — reads STACKONE_API_KEY and STACKONE_ACCOUNT_ID from env
const toolset = new StackOneToolSet();

// Filter for any relevant tools
const tools = await toolset.fetchTools({
Expand Down
7 changes: 4 additions & 3 deletions examples/claude-agent-sdk-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import { tool, createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk';
import { z } from 'zod';
import { TEST_BASE_URL } from '../mocks/constants';
import { StackOneToolSet } from '../src';

describe('claude-agent-sdk-integration example e2e', () => {
Expand All @@ -25,7 +26,7 @@ describe('claude-agent-sdk-integration example e2e', () => {
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',
baseUrl: TEST_BASE_URL,
});

// Fetch all tools for this account via MCP
Expand Down Expand Up @@ -61,7 +62,7 @@ describe('claude-agent-sdk-integration example e2e', () => {
it('should create MCP server with StackOne tools', async () => {
const toolset = new StackOneToolSet({
accountId: 'your-bamboohr-account-id',
baseUrl: 'https://api.stackone.com',
baseUrl: TEST_BASE_URL,
});

const tools = await toolset.fetchTools();
Expand Down Expand Up @@ -99,7 +100,7 @@ describe('claude-agent-sdk-integration example e2e', () => {
it('should execute tool handler directly', async () => {
const toolset = new StackOneToolSet({
accountId: 'your-bamboohr-account-id',
baseUrl: 'https://api.stackone.com',
baseUrl: TEST_BASE_URL,
});

const tools = await toolset.fetchTools();
Expand Down
10 changes: 2 additions & 8 deletions examples/claude-agent-sdk-integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,9 @@ if (!apiKey) {
process.exit(1);
}

// Replace with your actual account ID from StackOne dashboard
const accountId = 'your-hris-account-id';

const claudeAgentSdkIntegration = async (): Promise<void> => {
// Initialize StackOne
const toolset = new StackOneToolSet({
accountId,
baseUrl: process.env.STACKONE_BASE_URL ?? 'https://api.stackone.com',
});
// Initialize StackOne — reads STACKONE_API_KEY and STACKONE_ACCOUNT_ID from env
const toolset = new StackOneToolSet();

// Fetch tools from StackOne and convert to Claude Agent SDK format
const tools = await toolset.fetchTools();
Expand Down
4 changes: 2 additions & 2 deletions examples/fetch-tools-debug.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Interactive CLI Demo
* Fetch Tools Debug CLI
*
* This example demonstrates how to build an interactive CLI tool using
* @clack/prompts to dynamically discover and execute StackOne tools.
Expand All @@ -11,7 +11,7 @@
*
* Run with:
* ```bash
* node --env-files=.env examples/interactive-cli.ts
* npx tsx examples/fetch-tools-debug.ts
* ```
*/

Expand Down
5 changes: 3 additions & 2 deletions examples/fetch-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import { http, HttpResponse } from 'msw';
import { server } from '../mocks/node';
import { TEST_BASE_URL } from '../mocks/constants';
import { StackOneToolSet } from '../src';

describe('fetch-tools example e2e', () => {
Expand All @@ -20,7 +21,7 @@ describe('fetch-tools example e2e', () => {
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 }) => {
http.post(`${TEST_BASE_URL}/actions/rpc`, async ({ request }) => {
const body: unknown = await request.json();
assert(typeof body === 'object' && body !== null);
const { action } = body as Record<string, unknown>;
Expand All @@ -40,7 +41,7 @@ describe('fetch-tools example e2e', () => {
);

const toolset = new StackOneToolSet({
baseUrl: 'https://api.stackone.com',
baseUrl: TEST_BASE_URL,
});

// Example 1: Fetch all tools (without account filter)
Expand Down
10 changes: 4 additions & 6 deletions examples/fetch-tools.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Example: fetch the latest StackOne tool catalog with filtering options.
*
* Set `STACKONE_API_KEY` (and optionally `STACKONE_BASE_URL`) before running.
* Set `STACKONE_API_KEY` before running.
* By default the script exits early in test environments where a real key is
* not available.
*/
Expand All @@ -15,9 +15,7 @@ if (!apiKey) {
process.exit(1);
}

const toolset = new StackOneToolSet({
baseUrl: process.env.STACKONE_BASE_URL ?? 'https://api.stackone.com',
});
const toolset = new StackOneToolSet({});

// Example 1: Fetch all tools
console.log('\n=== Example 1: Fetch all tools ===');
Expand All @@ -33,9 +31,9 @@ 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'],
accountIds: ['your-account-id'],
});
console.log(`Loaded ${toolsByAccountsOption.length} tools for account-789`);
console.log(`Loaded ${toolsByAccountsOption.length} tools for your-account-id`);

// Example 4: Filter by providers
console.log('\n=== Example 4: Filter by providers ===');
Expand Down
Loading
Loading