Skip to content

Commit cc25602

Browse files
CahidArdaUpstash Boxclaude
authored
feat: add @spec2tools/sdk-tanstack package for generating TanStack AI tools (#13)
* feat: add @spec2tools/sdk-tanstack package for generating TanStack AI tools Creates a new package that converts OpenAPI specifications into TanStack AI ServerTool instances using toolDefinition().server() from @tanstack/ai. Mirrors the @spec2tools/sdk pattern but targets @tanstack/ai's chat() API. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(sdk-tanstack): add README, LICENSE, and convertToolsToCodeMode - Add README.md with usage examples for createTools and convertToolsToCodeMode - Add LICENSE (MIT) - Add convertToolsToCodeMode function that converts ServerTool[] into 2 code-mode tools (search + execute) using Monty Python interpreter, mirroring the same pattern as @spec2tools/sdk - Add codeMode option to createTools - Add @pydantic/monty as a dependency for code mode execution Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add examples workspace and sdk-tanstack-node example - Add examples/* glob to pnpm-workspace.yaml so examples are workspace packages that resolve monorepo deps via workspace:* - Add examples/sdk-tanstack-node: a minimal Node.js script that uses @spec2tools/sdk-tanstack + @tanstack/ai to answer prompts against a JSONPlaceholder OpenAPI spec Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: streamline chat logging and remove unused streamToText import * fix: print input in tool call end * feat(examples/sdk-tanstack-node): add code-mode and convert-code-mode examples - code-mode.ts: same OpenAPI spec but with codeMode: true (2 tools) - convert-code-mode.ts: convertToolsToCodeMode with hand-written mock tools - Update package.json with start:code-mode and start:convert scripts - Update README to document all three examples Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(examples/sdk-tanstack-node): prefix example files with example- Rename index.ts → example-basic.ts, code-mode.ts → example-code-mode.ts, convert-code-mode.ts → example-convert-code-mode.ts. Update package.json scripts and README accordingly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: resolve zod version conflicts across packages Move zod to peerDependencies with ^3.25.0 || ^4.0.0 in core, sdk, and sdk-tanstack to let consumers choose the version. Update internal imports to zod/v3 for compatibility. Fix excessively deep type instantiation in stdio-mcp by double-casting mcpClient.tools() result. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ci: add changeset and CI workflows Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: add changesets * fix(ci): remove explicit pnpm version to use packageManager field Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Upstash Box <box@upstash.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 334d5b2 commit cc25602

29 files changed

Lines changed: 1060 additions & 31 deletions

.changeset/plain-doodles-live.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@spec2tools/sdk-tanstack": patch
3+
---
4+
5+
Add sdk-tanstack package

.changeset/thirty-sheep-grow.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@spec2tools/stdio-mcp": patch
3+
"@spec2tools/core": patch
4+
"@spec2tools/sdk": patch
5+
---
6+
7+
Use zod/v3 instead of zod and make zod a peer dependency

.github/workflows/changeset.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: Changeset
2+
3+
on:
4+
push:
5+
branches: [main]
6+
7+
concurrency:
8+
group: ${{ github.workflow }}-${{ github.ref }}
9+
cancel-in-progress: false
10+
11+
jobs:
12+
release:
13+
runs-on: ubuntu-latest
14+
permissions:
15+
contents: write
16+
pull-requests: write
17+
18+
steps:
19+
- uses: actions/checkout@v4
20+
21+
- uses: pnpm/action-setup@v4
22+
23+
- uses: actions/setup-node@v4
24+
with:
25+
node-version: 22
26+
cache: pnpm
27+
28+
- run: pnpm install --frozen-lockfile
29+
30+
- run: pnpm build
31+
32+
- uses: changesets/action@v1
33+
with:
34+
version: pnpm changeset version
35+
createGithubReleases: true
36+
env:
37+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.github/workflows/ci.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
concurrency:
9+
group: ${{ github.workflow }}-${{ github.ref }}
10+
cancel-in-progress: true
11+
12+
jobs:
13+
ci:
14+
runs-on: ubuntu-latest
15+
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- uses: pnpm/action-setup@v4
20+
21+
- uses: actions/setup-node@v4
22+
with:
23+
node-version: 22
24+
cache: pnpm
25+
26+
- run: pnpm install --frozen-lockfile
27+
28+
- run: pnpm build
29+
30+
- run: pnpm test
31+
32+
- run: pnpm typecheck
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
OPENAI_API_KEY=sk-...
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# example: sdk-tanstack-node
2+
3+
Node.js examples that use `@spec2tools/sdk-tanstack` to generate [TanStack AI](https://tanstack.com/ai) tools, then run a chat with OpenAI.
4+
5+
## Setup
6+
7+
```bash
8+
cp .env.example .env
9+
# add your OPENAI_API_KEY to .env
10+
```
11+
12+
Install dependencies from the repo root:
13+
14+
```bash
15+
pnpm install
16+
```
17+
18+
---
19+
20+
## Examples
21+
22+
### 1. `example-basic.ts` — one tool per endpoint
23+
24+
Generates one `ServerTool` per OpenAPI operation and passes them all to `chat()`.
25+
26+
```bash
27+
pnpm start
28+
pnpm start "How many posts does user 1 have?"
29+
```
30+
31+
**How it works:**
32+
1. `createTools({ spec })` parses the OpenAPI spec and returns a `ServerTool[]` — one per operation
33+
2. The full tool list is passed to `chat()`; the model picks and calls whichever it needs
34+
3. Streamed chunks are printed to the console as they arrive
35+
36+
---
37+
38+
### 2. `example-code-mode.ts` — collapsed into 2 tools via `codeMode`
39+
40+
Passes `codeMode: true` to `createTools`, collapsing all endpoints into just two tools: `search` and `execute`. The model discovers endpoints with `search` then calls them by writing Python code in `execute`. Significantly reduces token usage for large APIs.
41+
42+
```bash
43+
pnpm start:code-mode
44+
pnpm start:code-mode "How many posts does user 1 have?"
45+
```
46+
47+
**How it works:**
48+
1. `createTools({ spec, codeMode: true })` returns exactly 2 `ServerTool` instances
49+
2. The model uses `search` to find relevant endpoints, then `execute` to call them via a sandboxed Python interpreter
50+
3. Same streaming output as the basic example
51+
52+
---
53+
54+
### 3. `example-convert-code-mode.ts``convertToolsToCodeMode` with hand-written tools
55+
56+
Shows that `convertToolsToCodeMode` works with **any** `ServerTool[]`, not only tools generated from an OpenAPI spec. Two simple math tools are created manually with `toolDefinition().server()`, then converted to code mode.
57+
58+
```bash
59+
pnpm start:convert
60+
pnpm start:convert "What is 42 multiplied by 7? Then add 15 to the result."
61+
```
62+
63+
**How it works:**
64+
1. Two `ServerTool` instances (`add`, `multiply`) are created with `toolDefinition().server()`
65+
2. `convertToolsToCodeMode([addTool, multiplyTool])` collapses them into `search` + `execute`
66+
3. The model writes Python code in `execute` to chain the tool calls together
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { chat } from '@tanstack/ai';
2+
import { openaiText } from '@tanstack/ai-openai';
3+
import { createTools } from '@spec2tools/sdk-tanstack';
4+
import path from 'path';
5+
import { fileURLToPath } from 'url';
6+
7+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
8+
9+
// Points to the sample OpenAPI spec bundled with @spec2tools/cli
10+
const SPEC = path.resolve(__dirname, '../../packages/cli/examples/sample-api.yaml');
11+
12+
const prompt = process.argv[2] ?? 'List the first 3 users and tell me their names.';
13+
14+
console.log(` > Prompt: ${prompt}\n`);
15+
16+
const tools = await createTools({ spec: SPEC });
17+
18+
const stream = chat({
19+
adapter: openaiText('gpt-4o-mini'),
20+
messages: [{ role: 'user', content: prompt }],
21+
tools,
22+
});
23+
24+
for await (const chunk of stream) {
25+
if (chunk.type === "TOOL_CALL_START") {
26+
console.log(` > Tool call started: ${chunk.toolName}\n`);
27+
} else if (chunk.type === "TOOL_CALL_END") {
28+
console.log(` > Tool call parameters: ${JSON.stringify(chunk.input)}\n`);
29+
} else if (chunk.type === "TEXT_MESSAGE_CONTENT") {
30+
process.stdout.write(chunk.delta); // Stream text content to console
31+
}
32+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { chat } from '@tanstack/ai';
2+
import { openaiText } from '@tanstack/ai-openai';
3+
import { createTools } from '@spec2tools/sdk-tanstack';
4+
import path from 'path';
5+
import { fileURLToPath } from 'url';
6+
7+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
8+
9+
// Points to the sample OpenAPI spec bundled with @spec2tools/cli
10+
const SPEC = path.resolve(__dirname, '../../packages/cli/examples/sample-api.yaml');
11+
12+
const prompt = process.argv[2] ?? 'List the first 3 users and tell me their names.';
13+
14+
console.log(` > Prompt: ${prompt}\n`);
15+
16+
// codeMode: true collapses all endpoints into 2 tools (search + execute),
17+
// reducing token usage significantly for large APIs
18+
const tools = await createTools({ spec: SPEC, codeMode: true });
19+
20+
console.log(` > Running with ${Object.keys(tools).length} code-mode tools: ${tools.map(t => t.name).join(', ')}\n`);
21+
22+
const stream = chat({
23+
adapter: openaiText('gpt-4o-mini'),
24+
messages: [{ role: 'user', content: prompt }],
25+
tools,
26+
});
27+
28+
for await (const chunk of stream) {
29+
if (chunk.type === 'TOOL_CALL_START') {
30+
console.log(` > Tool call started: ${chunk.toolName}\n`);
31+
} else if (chunk.type === 'TOOL_CALL_END') {
32+
console.log(` > Tool call parameters: ${JSON.stringify(chunk.input)}\n`);
33+
} else if (chunk.type === 'TEXT_MESSAGE_CONTENT') {
34+
process.stdout.write(chunk.delta);
35+
}
36+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { chat, toolDefinition, type JSONSchema } from '@tanstack/ai';
2+
import { openaiText } from '@tanstack/ai-openai';
3+
import { convertToolsToCodeMode } from '@spec2tools/sdk-tanstack';
4+
5+
const prompt =
6+
process.argv[2] ?? 'What is 42 multiplied by 7? Then add 15 to the result.';
7+
8+
console.log(` > Prompt: ${prompt}\n`);
9+
10+
// Mock tools — not from an OpenAPI spec, just plain ServerTool instances
11+
// created manually with toolDefinition().server()
12+
const addTool = toolDefinition({
13+
name: 'add',
14+
description: 'Add two numbers together',
15+
inputSchema: {
16+
type: 'object',
17+
properties: {
18+
a: { type: 'number', description: 'First number' },
19+
b: { type: 'number', description: 'Second number' },
20+
},
21+
required: ['a', 'b'],
22+
} as JSONSchema,
23+
}).server(async (args) => {
24+
const { a, b } = args as { a: number; b: number };
25+
return { result: a + b };
26+
});
27+
28+
const multiplyTool = toolDefinition({
29+
name: 'multiply',
30+
description: 'Multiply two numbers',
31+
inputSchema: {
32+
type: 'object',
33+
properties: {
34+
a: { type: 'number', description: 'First number' },
35+
b: { type: 'number', description: 'Second number' },
36+
},
37+
required: ['a', 'b'],
38+
} as JSONSchema,
39+
}).server(async (args) => {
40+
const { a, b } = args as { a: number; b: number };
41+
return { result: a * b };
42+
});
43+
44+
// convertToolsToCodeMode works with any ServerTool[], not just those from createTools
45+
const tools = convertToolsToCodeMode([addTool, multiplyTool]);
46+
47+
console.log(` > Running with ${tools.length} code-mode tools: ${tools.map(t => t.name).join(', ')}\n`);
48+
49+
const stream = chat({
50+
adapter: openaiText('gpt-4o-mini'),
51+
messages: [{ role: 'user', content: prompt }],
52+
tools,
53+
});
54+
55+
for await (const chunk of stream) {
56+
if (chunk.type === 'TOOL_CALL_START') {
57+
console.log(` > Tool call started: ${chunk.toolName}\n`);
58+
} else if (chunk.type === 'TOOL_CALL_END') {
59+
console.log(` > Tool call parameters: ${JSON.stringify(chunk.input)}\n`);
60+
} else if (chunk.type === 'TEXT_MESSAGE_CONTENT') {
61+
process.stdout.write(chunk.delta);
62+
}
63+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "example-sdk-tanstack-node",
3+
"version": "0.0.0",
4+
"private": true,
5+
"description": "Node.js example: generate TanStack AI tools from an OpenAPI spec",
6+
"type": "module",
7+
"scripts": {
8+
"start": "tsx example-basic.ts",
9+
"start:code-mode": "tsx example-code-mode.ts",
10+
"start:convert": "tsx example-convert-code-mode.ts",
11+
"typecheck": "tsc --noEmit"
12+
},
13+
"dependencies": {
14+
"@spec2tools/sdk-tanstack": "workspace:*",
15+
"@tanstack/ai": "^0.6.3",
16+
"@tanstack/ai-openai": "^0.6.0",
17+
"zod": "^4.0.0"
18+
},
19+
"devDependencies": {
20+
"@types/node": "^22.0.0",
21+
"tsx": "^4.0.0",
22+
"typescript": "^5.6.0"
23+
}
24+
}

0 commit comments

Comments
 (0)