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
5 changes: 5 additions & 0 deletions .changeset/plain-doodles-live.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@spec2tools/sdk-tanstack": patch
---

Add sdk-tanstack package
7 changes: 7 additions & 0 deletions .changeset/thirty-sheep-grow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@spec2tools/stdio-mcp": patch
"@spec2tools/core": patch
"@spec2tools/sdk": patch
---

Use zod/v3 instead of zod and make zod a peer dependency
37 changes: 37 additions & 0 deletions .github/workflows/changeset.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Changeset

on:
push:
branches: [main]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write

steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

- run: pnpm install --frozen-lockfile

- run: pnpm build

- uses: changesets/action@v1
with:
version: pnpm changeset version
createGithubReleases: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: CI

on:
push:
branches: [main]
pull_request:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
ci:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

- run: pnpm install --frozen-lockfile

- run: pnpm build

- run: pnpm test

- run: pnpm typecheck
1 change: 1 addition & 0 deletions examples/sdk-tanstack-node/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
OPENAI_API_KEY=sk-...
66 changes: 66 additions & 0 deletions examples/sdk-tanstack-node/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# example: sdk-tanstack-node

Node.js examples that use `@spec2tools/sdk-tanstack` to generate [TanStack AI](https://tanstack.com/ai) tools, then run a chat with OpenAI.

## Setup

```bash
cp .env.example .env
# add your OPENAI_API_KEY to .env
```

Install dependencies from the repo root:

```bash
pnpm install
```

---

## Examples

### 1. `example-basic.ts` — one tool per endpoint

Generates one `ServerTool` per OpenAPI operation and passes them all to `chat()`.

```bash
pnpm start
pnpm start "How many posts does user 1 have?"
```

**How it works:**
1. `createTools({ spec })` parses the OpenAPI spec and returns a `ServerTool[]` — one per operation
2. The full tool list is passed to `chat()`; the model picks and calls whichever it needs
3. Streamed chunks are printed to the console as they arrive

---

### 2. `example-code-mode.ts` — collapsed into 2 tools via `codeMode`

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.

```bash
pnpm start:code-mode
pnpm start:code-mode "How many posts does user 1 have?"
```

**How it works:**
1. `createTools({ spec, codeMode: true })` returns exactly 2 `ServerTool` instances
2. The model uses `search` to find relevant endpoints, then `execute` to call them via a sandboxed Python interpreter
3. Same streaming output as the basic example

---

### 3. `example-convert-code-mode.ts` — `convertToolsToCodeMode` with hand-written tools

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.

```bash
pnpm start:convert
pnpm start:convert "What is 42 multiplied by 7? Then add 15 to the result."
```

**How it works:**
1. Two `ServerTool` instances (`add`, `multiply`) are created with `toolDefinition().server()`
2. `convertToolsToCodeMode([addTool, multiplyTool])` collapses them into `search` + `execute`
3. The model writes Python code in `execute` to chain the tool calls together
32 changes: 32 additions & 0 deletions examples/sdk-tanstack-node/example-basic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { chat } from '@tanstack/ai';
import { openaiText } from '@tanstack/ai-openai';
import { createTools } from '@spec2tools/sdk-tanstack';
import path from 'path';
import { fileURLToPath } from 'url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

// Points to the sample OpenAPI spec bundled with @spec2tools/cli
const SPEC = path.resolve(__dirname, '../../packages/cli/examples/sample-api.yaml');

const prompt = process.argv[2] ?? 'List the first 3 users and tell me their names.';

console.log(` > Prompt: ${prompt}\n`);

const tools = await createTools({ spec: SPEC });

const stream = chat({
adapter: openaiText('gpt-4o-mini'),
messages: [{ role: 'user', content: prompt }],
tools,
});

for await (const chunk of stream) {
if (chunk.type === "TOOL_CALL_START") {
console.log(` > Tool call started: ${chunk.toolName}\n`);
} else if (chunk.type === "TOOL_CALL_END") {
console.log(` > Tool call parameters: ${JSON.stringify(chunk.input)}\n`);
} else if (chunk.type === "TEXT_MESSAGE_CONTENT") {
process.stdout.write(chunk.delta); // Stream text content to console
}
}
36 changes: 36 additions & 0 deletions examples/sdk-tanstack-node/example-code-mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { chat } from '@tanstack/ai';
import { openaiText } from '@tanstack/ai-openai';
import { createTools } from '@spec2tools/sdk-tanstack';
import path from 'path';
import { fileURLToPath } from 'url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

// Points to the sample OpenAPI spec bundled with @spec2tools/cli
const SPEC = path.resolve(__dirname, '../../packages/cli/examples/sample-api.yaml');

const prompt = process.argv[2] ?? 'List the first 3 users and tell me their names.';

console.log(` > Prompt: ${prompt}\n`);

// codeMode: true collapses all endpoints into 2 tools (search + execute),
// reducing token usage significantly for large APIs
const tools = await createTools({ spec: SPEC, codeMode: true });

console.log(` > Running with ${Object.keys(tools).length} code-mode tools: ${tools.map(t => t.name).join(', ')}\n`);

const stream = chat({
adapter: openaiText('gpt-4o-mini'),
messages: [{ role: 'user', content: prompt }],
tools,
});

for await (const chunk of stream) {
if (chunk.type === 'TOOL_CALL_START') {
console.log(` > Tool call started: ${chunk.toolName}\n`);
} else if (chunk.type === 'TOOL_CALL_END') {
console.log(` > Tool call parameters: ${JSON.stringify(chunk.input)}\n`);
} else if (chunk.type === 'TEXT_MESSAGE_CONTENT') {
process.stdout.write(chunk.delta);
}
}
63 changes: 63 additions & 0 deletions examples/sdk-tanstack-node/example-convert-code-mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { chat, toolDefinition, type JSONSchema } from '@tanstack/ai';
import { openaiText } from '@tanstack/ai-openai';
import { convertToolsToCodeMode } from '@spec2tools/sdk-tanstack';

const prompt =
process.argv[2] ?? 'What is 42 multiplied by 7? Then add 15 to the result.';

console.log(` > Prompt: ${prompt}\n`);

// Mock tools — not from an OpenAPI spec, just plain ServerTool instances
// created manually with toolDefinition().server()
const addTool = toolDefinition({
name: 'add',
description: 'Add two numbers together',
inputSchema: {
type: 'object',
properties: {
a: { type: 'number', description: 'First number' },
b: { type: 'number', description: 'Second number' },
},
required: ['a', 'b'],
} as JSONSchema,
}).server(async (args) => {
const { a, b } = args as { a: number; b: number };
return { result: a + b };
});

const multiplyTool = toolDefinition({
name: 'multiply',
description: 'Multiply two numbers',
inputSchema: {
type: 'object',
properties: {
a: { type: 'number', description: 'First number' },
b: { type: 'number', description: 'Second number' },
},
required: ['a', 'b'],
} as JSONSchema,
}).server(async (args) => {
const { a, b } = args as { a: number; b: number };
return { result: a * b };
});

// convertToolsToCodeMode works with any ServerTool[], not just those from createTools
const tools = convertToolsToCodeMode([addTool, multiplyTool]);

console.log(` > Running with ${tools.length} code-mode tools: ${tools.map(t => t.name).join(', ')}\n`);

const stream = chat({
adapter: openaiText('gpt-4o-mini'),
messages: [{ role: 'user', content: prompt }],
tools,
});

for await (const chunk of stream) {
if (chunk.type === 'TOOL_CALL_START') {
console.log(` > Tool call started: ${chunk.toolName}\n`);
} else if (chunk.type === 'TOOL_CALL_END') {
console.log(` > Tool call parameters: ${JSON.stringify(chunk.input)}\n`);
} else if (chunk.type === 'TEXT_MESSAGE_CONTENT') {
process.stdout.write(chunk.delta);
}
}
24 changes: 24 additions & 0 deletions examples/sdk-tanstack-node/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "example-sdk-tanstack-node",
"version": "0.0.0",
"private": true,
"description": "Node.js example: generate TanStack AI tools from an OpenAPI spec",
"type": "module",
"scripts": {
"start": "tsx example-basic.ts",
"start:code-mode": "tsx example-code-mode.ts",
"start:convert": "tsx example-convert-code-mode.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@spec2tools/sdk-tanstack": "workspace:*",
"@tanstack/ai": "^0.6.3",
"@tanstack/ai-openai": "^0.6.0",
"zod": "^4.0.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"tsx": "^4.0.0",
"typescript": "^5.6.0"
}
}
13 changes: 13 additions & 0 deletions examples/sdk-tanstack-node/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true
},
"include": ["*.ts"]
}
4 changes: 3 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,11 @@
"express": "^5.0.0",
"open": "^10.1.0",
"yaml": "^2.5.0",
"zod": "^3.24.0",
"zod-to-json-schema": "^3.24.0"
},
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
},
"devDependencies": {
"@types/express": "^5.0.0",
"@types/node": "^22.0.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/ai-tools.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, vi } from 'vitest';
import { z } from 'zod';
import { z } from 'zod/v3';
import { toAISDKTools } from './ai-tools.js';
import type { Tool } from './types.js';

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/code-mode.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, vi } from 'vitest';
import { z } from 'zod';
import { z } from 'zod/v3';
import { tool } from 'ai';
import { toCodeModeTools } from './code-mode.js';

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/code-mode.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { tool, generateText } from 'ai';
import { z } from 'zod';
import { z } from 'zod/v3';
import { zodToJsonSchema } from 'zod-to-json-schema';
import {
Monty,
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/openapi-parser.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { z } from 'zod';
import { z } from 'zod/v3';
import { readFile } from 'fs/promises';
import { parse as parseYaml } from 'yaml';
import {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/tool-executor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { z } from 'zod';
import { z } from 'zod/v3';
import { Tool, HttpMethod, AuthConfig, ParameterMetadata } from './types.js';
import { ToolExecutionError } from './errors.js';
import { AuthManager } from './auth-manager.js';
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { z } from 'zod';
import { z } from 'zod/v3';

export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';

Expand Down
Loading
Loading