Skip to content

Commit 3e40126

Browse files
committed
Merge branch 'main' into docs/add-anthropic-openai-responses-integration
Resolve README.md conflict by: - Keeping all integration examples (OpenAI, Anthropic, AI SDK, TanStack AI, Claude Agent SDK) - Reorganising structure: Installation → Usage → Integrations → Features - Using collapsible sections for integration examples - Removing dead link to non-existent custom-base-url.ts example
2 parents 648a1da + e890de1 commit 3e40126

31 files changed

+1739
-394
lines changed

.oxfmtrc.jsonc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
"useTabs": true,
44
"semi": true,
55
"singleQuote": true,
6+
"ignorePatterns": [".claude/settings.local.json"],
67
}

README.md

Lines changed: 124 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,21 @@ pnpm add @stackone/ai
2626
bun add @stackone/ai
2727
```
2828

29+
### Optional: AI SDK Integration
30+
31+
If you plan to use the AI SDK integration (Vercel AI SDK), install it separately:
32+
33+
```bash
34+
# Using npm
35+
npm install ai
36+
37+
# Using yarn
38+
yarn add ai
39+
40+
# Using pnpm
41+
pnpm add ai
42+
```
43+
2944
## Usage
3045

3146
```typescript
@@ -41,8 +56,6 @@ const employeeTool = tools.getTool("bamboohr_list_employees");
4156
const employees = await employeeTool.execute();
4257
```
4358

44-
[View full example](examples/index.ts)
45-
4659
### Authentication
4760

4861
Set the `STACKONE_API_KEY` environment variable:
@@ -223,6 +236,114 @@ await generateText({
223236

224237
</details>
225238

239+
<details>
240+
<summary><strong>With TanStack AI</strong></summary>
241+
242+
```bash
243+
npm install @tanstack/ai @tanstack/ai-openai zod # or: yarn/pnpm/bun add
244+
```
245+
246+
```typescript
247+
import { chat } from "@tanstack/ai";
248+
import { openai } from "@tanstack/ai-openai";
249+
import { z } from "zod";
250+
import { StackOneToolSet } from "@stackone/ai";
251+
252+
const toolset = new StackOneToolSet({
253+
baseUrl: "https://api.stackone.com",
254+
accountId: "your-account-id",
255+
});
256+
257+
const tools = await toolset.fetchTools();
258+
const employeeTool = tools.getTool("bamboohr_get_employee");
259+
260+
// TanStack AI requires Zod schemas for tool input validation
261+
const getEmployeeTool = {
262+
name: employeeTool.name,
263+
description: employeeTool.description,
264+
inputSchema: z.object({
265+
id: z.string().describe("The employee ID"),
266+
}),
267+
execute: async (args: { id: string }) => {
268+
return employeeTool.execute(args);
269+
},
270+
};
271+
272+
const adapter = openai();
273+
const stream = chat({
274+
adapter,
275+
model: "gpt-4o",
276+
messages: [{ role: "user", content: "Get employee with id: abc123" }],
277+
tools: [getEmployeeTool],
278+
});
279+
280+
for await (const chunk of stream) {
281+
// Process streaming chunks
282+
}
283+
```
284+
285+
[View full example](examples/tanstack-ai-integration.ts)
286+
287+
</details>
288+
289+
<details>
290+
<summary><strong>With Claude Agent SDK</strong></summary>
291+
292+
```bash
293+
npm install @anthropic-ai/claude-agent-sdk zod # or: yarn/pnpm/bun add
294+
```
295+
296+
```typescript
297+
import { query, tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
298+
import { z } from "zod";
299+
import { StackOneToolSet } from "@stackone/ai";
300+
301+
const toolset = new StackOneToolSet({
302+
baseUrl: "https://api.stackone.com",
303+
accountId: "your-account-id",
304+
});
305+
306+
const tools = await toolset.fetchTools();
307+
const employeeTool = tools.getTool("bamboohr_get_employee");
308+
309+
// Create a Claude Agent SDK tool from the StackOne tool
310+
const getEmployeeTool = tool(
311+
employeeTool.name,
312+
employeeTool.description,
313+
{ id: z.string().describe("The employee ID") },
314+
async (args) => {
315+
const result = await employeeTool.execute(args);
316+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
317+
}
318+
);
319+
320+
// Create an MCP server with the StackOne tool
321+
const mcpServer = createSdkMcpServer({
322+
name: "stackone-tools",
323+
version: "1.0.0",
324+
tools: [getEmployeeTool],
325+
});
326+
327+
// Use with Claude Agent SDK query
328+
const result = query({
329+
prompt: "Get the employee with id: abc123",
330+
options: {
331+
model: "claude-sonnet-4-5-20250929",
332+
mcpServers: { "stackone-tools": mcpServer },
333+
tools: [], // Disable built-in tools
334+
maxTurns: 3,
335+
},
336+
});
337+
338+
for await (const message of result) {
339+
// Process streaming messages
340+
}
341+
```
342+
343+
[View full example](examples/claude-agent-sdk-integration.ts)
344+
345+
</details>
346+
226347
## Features
227348

228349
### Filtering Tools with fetchTools()
@@ -269,7 +390,7 @@ This is especially useful when you want to:
269390

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

272-
> ⚠️ **Beta Feature**: Meta tools are currently in beta and the API may change in future versions.
393+
> **Beta Feature**: Meta tools are currently in beta and the API may change in future versions.
273394
274395
#### How Meta Tools Work
275396

@@ -345,8 +466,6 @@ import { StackOneToolSet } from "@stackone/ai";
345466
const toolset = new StackOneToolSet({ baseUrl: "https://api.example-dev.com" });
346467
```
347468

348-
[View full example](examples/custom-base-url.ts)
349-
350469
### Testing with dryRun
351470

352471
You can use the `dryRun` option to return the api arguments from a tool call without making the actual api call:
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* E2E test for ai-sdk-integration.ts example
3+
*
4+
* Tests the complete flow of using StackOne tools with the AI SDK.
5+
*/
6+
7+
import { openai } from '@ai-sdk/openai';
8+
import { generateText, stepCountIs } from 'ai';
9+
import { StackOneToolSet } from '../src';
10+
11+
describe('ai-sdk-integration example e2e', () => {
12+
beforeEach(() => {
13+
vi.stubEnv('STACKONE_API_KEY', 'test-key');
14+
vi.stubEnv('OPENAI_API_KEY', 'test-openai-key');
15+
});
16+
17+
afterEach(() => {
18+
vi.unstubAllEnvs();
19+
});
20+
21+
it('should fetch tools, convert to AI SDK format, and generate text with tool calls', async () => {
22+
const toolset = new StackOneToolSet({
23+
accountId: 'your-bamboohr-account-id',
24+
baseUrl: 'https://api.stackone.com',
25+
});
26+
27+
// Fetch all tools for this account via MCP
28+
const tools = await toolset.fetchTools();
29+
expect(tools.length).toBeGreaterThan(0);
30+
31+
// Convert to AI SDK tools
32+
const aiSdkTools = await tools.toAISDK();
33+
expect(aiSdkTools).toBeDefined();
34+
expect(Object.keys(aiSdkTools).length).toBeGreaterThan(0);
35+
36+
// Verify the tools have the expected structure
37+
const toolNames = Object.keys(aiSdkTools);
38+
expect(toolNames).toContain('bamboohr_list_employees');
39+
expect(toolNames).toContain('bamboohr_get_employee');
40+
41+
// The AI SDK will automatically call the tool if needed
42+
const { text } = await generateText({
43+
model: openai('gpt-5'),
44+
tools: aiSdkTools,
45+
prompt: 'Get all details about employee with id: c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA',
46+
stopWhen: stepCountIs(3),
47+
});
48+
49+
// The mocked OpenAI response includes 'Michael' in the text
50+
expect(text).toContain('Michael');
51+
});
52+
});

examples/ai-sdk-integration.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
/**
22
* This example shows how to use StackOne tools with the AI SDK.
3+
*
4+
* The AI SDK provides an agent-like pattern through the `stopWhen` parameter
5+
* with `stepCountIs()`. This creates a multi-step tool loop where the model
6+
* can autonomously call tools and reason over results until the stop condition
7+
* is met.
8+
*
9+
* In AI SDK v6+, you can use the `ToolLoopAgent` class for more explicit
10+
* agent functionality.
311
*/
412

513
import assert from 'node:assert';
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/**
2+
* E2E test for claude-agent-sdk-integration.ts example
3+
*
4+
* Tests the setup of StackOne tools with Claude Agent SDK.
5+
*
6+
* Note: The Claude Agent SDK spawns a subprocess to run claude-code, which
7+
* requires the ANTHROPIC_API_KEY environment variable and a running claude-code
8+
* installation. This test validates the tool setup and MCP server creation,
9+
* but does not test the actual query execution.
10+
*/
11+
12+
import { tool, createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk';
13+
import { z } from 'zod';
14+
import { StackOneToolSet } from '../src';
15+
16+
describe('claude-agent-sdk-integration example e2e', () => {
17+
beforeEach(() => {
18+
vi.stubEnv('STACKONE_API_KEY', 'test-key');
19+
});
20+
21+
afterEach(() => {
22+
vi.unstubAllEnvs();
23+
});
24+
25+
it('should fetch tools and create Claude Agent SDK tool wrapper', async () => {
26+
const toolset = new StackOneToolSet({
27+
accountId: 'your-bamboohr-account-id',
28+
baseUrl: 'https://api.stackone.com',
29+
});
30+
31+
// Fetch all tools for this account via MCP
32+
const tools = await toolset.fetchTools();
33+
expect(tools.length).toBeGreaterThan(0);
34+
35+
// Get a specific tool
36+
const employeeTool = tools.getTool('bamboohr_get_employee');
37+
expect(employeeTool).toBeDefined();
38+
assert(employeeTool !== undefined);
39+
40+
// Create Claude Agent SDK tool from StackOne tool
41+
const getEmployeeTool = tool(
42+
employeeTool.name,
43+
employeeTool.description,
44+
{
45+
id: z.string().describe('The employee ID'),
46+
},
47+
async (args) => {
48+
const result = await employeeTool.execute(args);
49+
return {
50+
content: [{ type: 'text' as const, text: JSON.stringify(result) }],
51+
};
52+
},
53+
);
54+
55+
expect(getEmployeeTool.name).toBe('bamboohr_get_employee');
56+
expect(getEmployeeTool.description).toContain('employee');
57+
expect(getEmployeeTool.inputSchema).toHaveProperty('id');
58+
expect(typeof getEmployeeTool.handler).toBe('function');
59+
});
60+
61+
it('should create MCP server with StackOne tools', async () => {
62+
const toolset = new StackOneToolSet({
63+
accountId: 'your-bamboohr-account-id',
64+
baseUrl: 'https://api.stackone.com',
65+
});
66+
67+
const tools = await toolset.fetchTools();
68+
const employeeTool = tools.getTool('bamboohr_get_employee');
69+
assert(employeeTool !== undefined);
70+
71+
// Create Claude Agent SDK tool
72+
const getEmployeeTool = tool(
73+
employeeTool.name,
74+
employeeTool.description,
75+
{
76+
id: z.string().describe('The employee ID'),
77+
},
78+
async (args) => {
79+
const result = await employeeTool.execute(args);
80+
return {
81+
content: [{ type: 'text' as const, text: JSON.stringify(result) }],
82+
};
83+
},
84+
);
85+
86+
// Create an MCP server with the StackOne tool
87+
const mcpServer = createSdkMcpServer({
88+
name: 'stackone-tools',
89+
version: '1.0.0',
90+
tools: [getEmployeeTool],
91+
});
92+
93+
// Verify MCP server was created
94+
expect(mcpServer).toBeDefined();
95+
expect(mcpServer.name).toBe('stackone-tools');
96+
expect(mcpServer.instance).toBeDefined();
97+
});
98+
99+
it('should execute tool handler directly', async () => {
100+
const toolset = new StackOneToolSet({
101+
accountId: 'your-bamboohr-account-id',
102+
baseUrl: 'https://api.stackone.com',
103+
});
104+
105+
const tools = await toolset.fetchTools();
106+
const employeeTool = tools.getTool('bamboohr_get_employee');
107+
assert(employeeTool !== undefined);
108+
109+
// Create Claude Agent SDK tool
110+
const getEmployeeTool = tool(
111+
employeeTool.name,
112+
employeeTool.description,
113+
{
114+
id: z.string().describe('The employee ID'),
115+
},
116+
async (args) => {
117+
const result = await employeeTool.execute(args);
118+
return {
119+
content: [{ type: 'text' as const, text: JSON.stringify(result) }],
120+
};
121+
},
122+
);
123+
124+
// Execute the tool handler directly
125+
const result = await getEmployeeTool.handler(
126+
{ id: 'c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA' },
127+
{} as unknown,
128+
);
129+
130+
expect(result).toBeDefined();
131+
expect(result.content).toHaveLength(1);
132+
expect(result.content[0]?.type).toBe('text');
133+
134+
// Parse the result text and verify it contains employee data
135+
const textContent = result.content[0];
136+
assert(textContent?.type === 'text');
137+
const data = JSON.parse(textContent.text) as unknown;
138+
expect(data).toHaveProperty('data');
139+
});
140+
});

0 commit comments

Comments
 (0)