Skip to content

Commit e890de1

Browse files
authored
feat(examples): add TanStack AI and Claude Agent SDK integrations with E2E tests (#222)
* chore: remove unused sample-document * chore: remove fetch tools example * Revert "chore: remove fetch tools example" This reverts commit e7372a4. * feat(example): update interactive fetchTool debug * 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. * 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. * 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 * 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 * 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 * 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 * chore(oxfmt): ignore .claude/settings.local.json * chore(deps): move @ai-sdk/provider to catalog:dev * docs: tanstack ai jsonschema * 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. * 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. * 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. * 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. * 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 * 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 * 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 * 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 * 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 * 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 * docs:
1 parent 134b0c2 commit e890de1

27 files changed

+1506
-280
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: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,100 @@ await generateText({
113113

114114
[View full example](examples/ai-sdk-integration.ts)
115115

116+
### TanStack AI
117+
118+
```typescript
119+
import { chat } from "@tanstack/ai";
120+
import { openai } from "@tanstack/ai-openai";
121+
import { z } from "zod";
122+
import { StackOneToolSet } from "@stackone/ai";
123+
124+
const toolset = new StackOneToolSet({
125+
baseUrl: "https://api.stackone.com",
126+
accountId: "your-account-id",
127+
});
128+
129+
const tools = await toolset.fetchTools();
130+
const employeeTool = tools.getTool("bamboohr_get_employee");
131+
132+
// TanStack AI requires Zod schemas for tool input validation
133+
const getEmployeeTool = {
134+
name: employeeTool.name,
135+
description: employeeTool.description,
136+
inputSchema: z.object({
137+
id: z.string().describe("The employee ID"),
138+
}),
139+
execute: async (args: { id: string }) => {
140+
return employeeTool.execute(args);
141+
},
142+
};
143+
144+
const adapter = openai();
145+
const stream = chat({
146+
adapter,
147+
model: "gpt-4o",
148+
messages: [{ role: "user", content: "Get employee with id: abc123" }],
149+
tools: [getEmployeeTool],
150+
});
151+
152+
for await (const chunk of stream) {
153+
// Process streaming chunks
154+
}
155+
```
156+
157+
[View full example](examples/tanstack-ai-integration.ts)
158+
159+
### Claude Agent SDK
160+
161+
```typescript
162+
import { query, tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
163+
import { z } from "zod";
164+
import { StackOneToolSet } from "@stackone/ai";
165+
166+
const toolset = new StackOneToolSet({
167+
baseUrl: "https://api.stackone.com",
168+
accountId: "your-account-id",
169+
});
170+
171+
const tools = await toolset.fetchTools();
172+
const employeeTool = tools.getTool("bamboohr_get_employee");
173+
174+
// Create a Claude Agent SDK tool from the StackOne tool
175+
const getEmployeeTool = tool(
176+
employeeTool.name,
177+
employeeTool.description,
178+
{ id: z.string().describe("The employee ID") },
179+
async (args) => {
180+
const result = await employeeTool.execute(args);
181+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
182+
}
183+
);
184+
185+
// Create an MCP server with the StackOne tool
186+
const mcpServer = createSdkMcpServer({
187+
name: "stackone-tools",
188+
version: "1.0.0",
189+
tools: [getEmployeeTool],
190+
});
191+
192+
// Use with Claude Agent SDK query
193+
const result = query({
194+
prompt: "Get the employee with id: abc123",
195+
options: {
196+
model: "claude-sonnet-4-5-20250929",
197+
mcpServers: { "stackone-tools": mcpServer },
198+
tools: [], // Disable built-in tools
199+
maxTurns: 3,
200+
},
201+
});
202+
203+
for await (const message of result) {
204+
// Process streaming messages
205+
}
206+
```
207+
208+
[View full example](examples/claude-agent-sdk-integration.ts)
209+
116210
## Usage
117211

118212
```typescript
@@ -128,8 +222,6 @@ const employeeTool = tools.getTool("bamboohr_list_employees");
128222
const employees = await employeeTool.execute();
129223
```
130224

131-
[View full example](examples/index.ts)
132-
133225
### Authentication
134226

135227
Set the `STACKONE_API_KEY` environment variable:
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)