From 1a1312ac8c3a9488858c0bd202bb3bdbb6260c96 Mon Sep 17 00:00:00 2001 From: Upstash Box Date: Thu, 12 Mar 2026 22:33:22 +0000 Subject: [PATCH 01/11] 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 --- packages/sdk-tanstack/package.json | 57 ++++++++++++++++++++++ packages/sdk-tanstack/src/index.ts | 1 + packages/sdk-tanstack/src/lib.test.ts | 65 +++++++++++++++++++++++++ packages/sdk-tanstack/src/lib.ts | 70 +++++++++++++++++++++++++++ packages/sdk-tanstack/tsconfig.json | 12 +++++ pnpm-lock.yaml | 46 ++++++++++++++++++ 6 files changed, 251 insertions(+) create mode 100644 packages/sdk-tanstack/package.json create mode 100644 packages/sdk-tanstack/src/index.ts create mode 100644 packages/sdk-tanstack/src/lib.test.ts create mode 100644 packages/sdk-tanstack/src/lib.ts create mode 100644 packages/sdk-tanstack/tsconfig.json diff --git a/packages/sdk-tanstack/package.json b/packages/sdk-tanstack/package.json new file mode 100644 index 0000000..03105e1 --- /dev/null +++ b/packages/sdk-tanstack/package.json @@ -0,0 +1,57 @@ +{ + "name": "@spec2tools/sdk-tanstack", + "version": "0.1.0", + "description": "Create TanStack AI tools from OpenAPI specifications", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "dist/**/*.js", + "dist/**/*.d.ts" + ], + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest" + }, + "keywords": [ + "openapi", + "ai", + "agent", + "tools", + "tanstack", + "tanstack-ai" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/CahidArda/spec2tools.git", + "directory": "packages/sdk-tanstack" + }, + "dependencies": { + "@spec2tools/core": "workspace:*", + "@tanstack/ai": "^0.6.3", + "zod": "^3.24.0", + "zod-to-json-schema": "^3.24.0" + }, + "peerDependencies": { + "@tanstack/ai": "^0.6.3", + "zod": "^3.24.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.6.0", + "vitest": "^4.0.18" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/packages/sdk-tanstack/src/index.ts b/packages/sdk-tanstack/src/index.ts new file mode 100644 index 0000000..0b7c504 --- /dev/null +++ b/packages/sdk-tanstack/src/index.ts @@ -0,0 +1 @@ +export { createTools, type Spec2ToolsOptions } from './lib.js'; diff --git a/packages/sdk-tanstack/src/lib.test.ts b/packages/sdk-tanstack/src/lib.test.ts new file mode 100644 index 0000000..e9dd8c0 --- /dev/null +++ b/packages/sdk-tanstack/src/lib.test.ts @@ -0,0 +1,65 @@ +import { describe, it, expect } from 'vitest'; +import { createTools } from './lib.js'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const SAMPLE_SPEC = path.resolve(__dirname, '../../cli/examples/sample-api.yaml'); + +describe('createTools', () => { + it('returns an array with one tool per operation', async () => { + const tools = await createTools({ spec: SAMPLE_SPEC }); + const names = tools.map((t) => t.name); + + expect(names).toContain('listUsers'); + expect(names).toContain('getUser'); + expect(names).toContain('createUser'); + expect(names).toContain('updateUser'); + expect(names).toContain('deleteUser'); + expect(names).toContain('listPosts'); + expect(names).toContain('createPost'); + expect(names).toContain('getPost'); + expect(names.length).toBe(8); + }); + + it('each tool has name, description, inputSchema, and execute', async () => { + const tools = await createTools({ spec: SAMPLE_SPEC }); + + for (const tool of tools) { + expect(tool.name, `${tool.name} missing name`).toBeTruthy(); + expect(tool.description, `${tool.name} missing description`).toBeTruthy(); + expect(tool.inputSchema, `${tool.name} missing inputSchema`).toBeDefined(); + expect(tool.execute, `${tool.name} missing execute`).toBeDefined(); + } + }); + + it('tools are marked as server-side', async () => { + const tools = await createTools({ spec: SAMPLE_SPEC }); + + for (const tool of tools) { + expect((tool as { __toolSide: string }).__toolSide).toBe('server'); + } + }); + + it('can execute getUser and receive a response', async () => { + const tools = await createTools({ spec: SAMPLE_SPEC }); + const getUser = tools.find((t) => t.name === 'getUser'); + + expect(getUser).toBeDefined(); + const result = await getUser!.execute!({ id: 1 }) as Record; + + expect(result).toHaveProperty('id', 1); + expect(result).toHaveProperty('name'); + }); + + it('can execute listUsers with query params', async () => { + const tools = await createTools({ spec: SAMPLE_SPEC }); + const listUsers = tools.find((t) => t.name === 'listUsers'); + + expect(listUsers).toBeDefined(); + const result = await listUsers!.execute!({ _limit: 2 }) as unknown[]; + + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBeLessThanOrEqual(2); + }); +}); diff --git a/packages/sdk-tanstack/src/lib.ts b/packages/sdk-tanstack/src/lib.ts new file mode 100644 index 0000000..677b826 --- /dev/null +++ b/packages/sdk-tanstack/src/lib.ts @@ -0,0 +1,70 @@ +import { + loadOpenAPISpec, + extractBaseUrl, + extractAuthConfig, + parseOperations, + createExecutableTools, + AuthManager, +} from '@spec2tools/core'; +import { toolDefinition, type ServerTool, type JSONSchema } from '@tanstack/ai'; +import { zodToJsonSchema } from 'zod-to-json-schema'; + +export interface Spec2ToolsOptions { + /** Path or URL to OpenAPI specification */ + spec: string; +} + +/** + * Create TanStack AI server tools from an OpenAPI specification. + * + * Each OpenAPI operation is converted into a TanStack AI `ServerTool` that can + * be passed directly to `chat()` from `@tanstack/ai`. + * + * @example + * ```ts + * import { createTools } from '@spec2tools/sdk-tanstack'; + * import { chat, toServerSentEventsResponse } from '@tanstack/ai'; + * import { openai } from '@tanstack/ai-openai'; + * + * const tools = await createTools({ spec: './openapi.yaml' }); + * + * const stream = chat({ + * adapter: openai(), + * model: 'gpt-4o', + * messages, + * tools, + * }); + * + * return toServerSentEventsResponse(stream); + * ``` + * + * @throws Error if the API requires authentication + */ +export async function createTools(options: Spec2ToolsOptions): Promise { + const spec = await loadOpenAPISpec(options.spec); + const baseUrl = extractBaseUrl(spec); + const authConfig = extractAuthConfig(spec); + + if (authConfig.type !== 'none') { + throw new Error( + `This API requires authentication (${authConfig.type}). ` + + `The createTools() function only supports APIs without authentication. ` + + `Use the CLI for authenticated APIs: npx @spec2tools/cli start --spec ${options.spec}` + ); + } + + const toolDefs = parseOperations(spec); + const authManager = new AuthManager(authConfig); + const tools = createExecutableTools(toolDefs, baseUrl, authManager); + + return tools.map((t) => { + const inputSchema = zodToJsonSchema(t.parameters) as JSONSchema; + return toolDefinition({ + name: t.name, + description: t.description, + inputSchema, + }).server(async (params) => { + return t.execute(params); + }); + }); +} diff --git a/packages/sdk-tanstack/tsconfig.json b/packages/sdk-tanstack/tsconfig.json new file mode 100644 index 0000000..d5f1fd0 --- /dev/null +++ b/packages/sdk-tanstack/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "src/**/*.test.ts"], + "references": [ + { "path": "../core" } + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c9626c4..b3275e8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -102,6 +102,31 @@ importers: specifier: ^4.0.18 version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.7)(yaml@2.8.2) + packages/sdk-tanstack: + dependencies: + '@spec2tools/core': + specifier: workspace:* + version: link:../core + '@tanstack/ai': + specifier: ^0.6.3 + version: 0.6.3 + zod: + specifier: ^3.24.0 + version: 3.25.76 + zod-to-json-schema: + specifier: ^3.24.0 + version: 3.25.1(zod@3.25.76) + devDependencies: + '@types/node': + specifier: ^22.0.0 + version: 22.19.7 + typescript: + specifier: ^5.6.0 + version: 5.9.3 + vitest: + specifier: ^4.0.18 + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.7)(yaml@2.8.2) + packages/stdio-mcp: dependencies: '@ai-sdk/mcp': @@ -614,6 +639,15 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@tanstack/ai@0.6.3': + resolution: {integrity: sha512-uCuP27PScfJXAKS/laqinlVSFSzPc2RMgXj0d7S7f6aqoIP/ack/0t5HjLdFgRMEOJ1wxbYmUV5Ln94wYg5t8Q==} + engines: {node: '>=18'} + + '@tanstack/devtools-event-client@0.4.3': + resolution: {integrity: sha512-OZI6QyULw0FI0wjgmeYzCIfbgPsOEzwJtCpa69XrfLMtNXLGnz3d/dIabk7frg0TmHo+Ah49w5I4KC7Tufwsvw==} + engines: {node: '>=18'} + hasBin: true + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -1250,6 +1284,9 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + partial-json@0.1.7: + resolution: {integrity: sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -2068,6 +2105,13 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@tanstack/ai@0.6.3': + dependencies: + '@tanstack/devtools-event-client': 0.4.3 + partial-json: 0.1.7 + + '@tanstack/devtools-event-client@0.4.3': {} + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -2701,6 +2745,8 @@ snapshots: parseurl@1.3.3: {} + partial-json@0.1.7: {} + path-exists@4.0.0: {} path-key@3.1.1: {} From e5d5b506dc9bfdc03ea6b5a5350eec1c88caf151 Mon Sep 17 00:00:00 2001 From: Upstash Box Date: Thu, 12 Mar 2026 22:51:09 +0000 Subject: [PATCH 02/11] 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 --- packages/sdk-tanstack/LICENSE | 21 +++ packages/sdk-tanstack/README.md | 77 +++++++++ packages/sdk-tanstack/package.json | 1 + packages/sdk-tanstack/src/index.ts | 2 +- packages/sdk-tanstack/src/lib.ts | 256 ++++++++++++++++++++++++++++- pnpm-lock.yaml | 3 + 6 files changed, 355 insertions(+), 5 deletions(-) create mode 100644 packages/sdk-tanstack/LICENSE create mode 100644 packages/sdk-tanstack/README.md diff --git a/packages/sdk-tanstack/LICENSE b/packages/sdk-tanstack/LICENSE new file mode 100644 index 0000000..e774a73 --- /dev/null +++ b/packages/sdk-tanstack/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2026 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/sdk-tanstack/README.md b/packages/sdk-tanstack/README.md new file mode 100644 index 0000000..5787170 --- /dev/null +++ b/packages/sdk-tanstack/README.md @@ -0,0 +1,77 @@ +# @spec2tools/sdk-tanstack + +Create TanStack AI tools from OpenAPI specifications. + +## Installation + +```bash +npm install @spec2tools/sdk-tanstack @tanstack/ai +``` + +## Usage + +```ts +import { createTools } from '@spec2tools/sdk-tanstack'; +import { chat, toServerSentEventsResponse } from '@tanstack/ai'; +import { openai } from '@tanstack/ai-openai'; + +const tools = await createTools({ spec: './openapi.yaml' }); + +const stream = chat({ + adapter: openai(), + model: 'gpt-4o', + messages, + tools, +}); + +return toServerSentEventsResponse(stream); +``` + +### Code Mode + +Enable code mode to collapse all endpoints into 2 tools (`search` + `execute`), reducing token usage by ~99.9%: + +```ts +const tools = await createTools({ + spec: './openapi.yaml', + codeMode: true, +}); +``` + +### Converting Existing Tools to Code Mode + +Use `convertToolsToCodeMode` to apply code mode to any existing `ServerTool` array: + +```ts +import { convertToolsToCodeMode } from '@spec2tools/sdk-tanstack'; + +const tools = await createTools({ spec: './openapi.yaml' }); +const codeModeTools = convertToolsToCodeMode(tools); +``` + +## API + +### `createTools(options: Spec2ToolsOptions): Promise` + +Creates TanStack AI server tools from an OpenAPI specification. + +#### Options + +- `spec` (string, required): Path or URL to the OpenAPI specification file (JSON or YAML) +- `codeMode` (boolean, optional): When `true`, returns 2 code-mode tools instead of one per endpoint + +#### Returns + +A `Promise` that resolves to an array of TanStack AI `ServerTool` instances, ready to pass to `chat()`. + +#### Throws + +- `Error` if the API requires authentication (unless `codeMode` is enabled). For authenticated APIs, use the `@spec2tools/cli` package instead. + +### `convertToolsToCodeMode(tools: ServerTool[]): ServerTool[]` + +Converts an existing TanStack AI `ServerTool` array into code mode (2 tools: `search` + `execute`). Useful when you have tools from another source and want to reduce token usage. + +## License + +MIT diff --git a/packages/sdk-tanstack/package.json b/packages/sdk-tanstack/package.json index 03105e1..4585911 100644 --- a/packages/sdk-tanstack/package.json +++ b/packages/sdk-tanstack/package.json @@ -37,6 +37,7 @@ "directory": "packages/sdk-tanstack" }, "dependencies": { + "@pydantic/monty": "^0.0.7", "@spec2tools/core": "workspace:*", "@tanstack/ai": "^0.6.3", "zod": "^3.24.0", diff --git a/packages/sdk-tanstack/src/index.ts b/packages/sdk-tanstack/src/index.ts index 0b7c504..28339f3 100644 --- a/packages/sdk-tanstack/src/index.ts +++ b/packages/sdk-tanstack/src/index.ts @@ -1 +1 @@ -export { createTools, type Spec2ToolsOptions } from './lib.js'; +export { createTools, convertToolsToCodeMode, type Spec2ToolsOptions } from './lib.js'; diff --git a/packages/sdk-tanstack/src/lib.ts b/packages/sdk-tanstack/src/lib.ts index 677b826..8453a1f 100644 --- a/packages/sdk-tanstack/src/lib.ts +++ b/packages/sdk-tanstack/src/lib.ts @@ -8,12 +8,251 @@ import { } from '@spec2tools/core'; import { toolDefinition, type ServerTool, type JSONSchema } from '@tanstack/ai'; import { zodToJsonSchema } from 'zod-to-json-schema'; +import { z } from 'zod'; +import { + Monty, + MontySnapshot, + MontyComplete, + MontyRuntimeError, + MontySyntaxError, + MontyTypingError, +} from '@pydantic/monty'; export interface Spec2ToolsOptions { /** Path or URL to OpenAPI specification */ spec: string; + /** Use code mode (2 tools: search + execute) instead of one tool per endpoint */ + codeMode?: boolean; +} + +// ─── Code Mode ─────────────────────────────────────────────────────────────── + +interface ToolIndexEntry { + originalName: string; + pyName: string; + description: string; + signature: string; + example: string; + searchText: string; +} + +function toPythonName(name: string): string { + let py = name.replace(/[^a-zA-Z0-9_]/g, '_'); + if (/^[0-9]/.test(py)) py = `_${py}`; + return py || '_unnamed'; +} + +function jsonSchemaTypeToPython(schema: Record): string { + if (!schema) return 'any'; + const type = schema.type as string | undefined; + if (type === 'string') return 'str'; + if (type === 'integer') return 'int'; + if (type === 'number') return 'float'; + if (type === 'boolean') return 'bool'; + if (type === 'array') return 'list'; + if (type === 'object') return 'dict'; + if (schema.enum) return 'str'; + return 'any'; +} + +function buildIndex(tools: ServerTool[]): ToolIndexEntry[] { + const index: ToolIndexEntry[] = []; + + for (const t of tools) { + const pyName = toPythonName(t.name); + const description = t.description ?? ''; + const params: { name: string; type: string; required: boolean }[] = []; + + try { + const schema = t.inputSchema as JSONSchema | undefined; + if (schema && typeof schema === 'object' && schema.properties) { + const required = new Set( + Array.isArray(schema.required) ? (schema.required as string[]) : [] + ); + for (const [pName, pSchema] of Object.entries(schema.properties)) { + params.push({ + name: pName, + type: jsonSchemaTypeToPython(pSchema as Record), + required: required.has(pName), + }); + } + } + } catch { + // Cannot introspect schema; proceed with empty params + } + + const paramStrs = params.map((p) => + p.required ? `${p.name}: ${p.type}` : `${p.name}: ${p.type} = None` + ); + const signature = `${pyName}(${paramStrs.join(', ')})`; + + const exampleParams = params + .filter((p) => p.required) + .slice(0, 2) + .map((p) => { + if (p.type === 'str') return `${p.name}="..."`; + if (p.type === 'int' || p.type === 'float') return `${p.name}=1`; + if (p.type === 'bool') return `${p.name}=True`; + return `${p.name}=...`; + }); + const example = `${pyName}(${exampleParams.join(', ')})`; + + index.push({ + originalName: t.name, + pyName, + description, + signature, + example, + searchText: `${t.name} ${pyName} ${description}`.toLowerCase(), + }); + } + + return index; +} + +function montyToJson(value: unknown): unknown { + if (value instanceof Map) { + const obj: Record = {}; + for (const [k, v] of value) { + obj[String(k)] = montyToJson(v); + } + return obj; + } + if (Array.isArray(value)) { + return value.map(montyToJson); + } + return value; } +/** + * Convert an existing TanStack AI ServerTool array into code mode + * (2 tools: `search` + `execute`). + * + * Useful when you already have tools from another source and want to reduce + * token usage by collapsing them into code mode. + * + * @example + * ```ts + * import { createTools, convertToolsToCodeMode } from '@spec2tools/sdk-tanstack'; + * + * const tools = await createTools({ spec: './openapi.yaml' }); + * const codeModeTools = convertToolsToCodeMode(tools); + * ``` + */ +export function convertToolsToCodeMode(tools: ServerTool[]): ServerTool[] { + const index = buildIndex(tools); + + const pyToOriginal = new Map(); + for (const entry of index) { + pyToOriginal.set(entry.pyName, entry.originalName); + } + const pythonNames = [...pyToOriginal.keys()]; + + const toolMap = new Map(tools.map((t) => [t.name, t])); + + const searchTool = toolDefinition({ + name: 'search', + description: + 'Search for available API endpoints by keyword. Returns matching ' + + 'function signatures with parameter types and usage examples. ' + + 'Use this to discover what functions are available before writing code.', + inputSchema: zodToJsonSchema( + z.object({ + query: z + .string() + .describe('Keywords to search for in endpoint names and descriptions'), + }) + ) as JSONSchema, + }).server(async (args) => { + const { query } = args as { query: string }; + const keywords = query.toLowerCase().split(/\s+/).filter(Boolean); + const matches = index.filter((entry) => + keywords.some((kw) => entry.searchText.includes(kw)) + ); + if (matches.length === 0) { + return `No endpoints found matching "${query}". Try different keywords.`; + } + return matches + .map((m) => `${m.signature}\n ${m.description}\n Example: ${m.example}`) + .join('\n\n'); + }); + + const executeTool = toolDefinition({ + name: 'execute', + description: + 'Execute Python code that calls API endpoints. Functions discovered ' + + 'via search are available to call directly. Always use keyword arguments.\n' + + 'Example:\n' + + ' users = listUsers(limit=10)\n' + + ' user = getUser(id="123")\n' + + ' users', + inputSchema: zodToJsonSchema( + z.object({ + code: z + .string() + .describe( + 'Python code to execute. Use keyword arguments when calling API functions.' + ), + }) + ) as JSONSchema, + }).server(async (args) => { + const { code } = args as { code: string }; + try { + const m = new Monty(code, { externalFunctions: pythonNames }); + let progress: MontySnapshot | MontyComplete = m.start({ + limits: { maxDurationSecs: 30 }, + }); + + while (progress instanceof MontySnapshot) { + const snapshot = progress; + const originalName = pyToOriginal.get(snapshot.functionName); + const serverTool = originalName ? toolMap.get(originalName) : undefined; + + if (!serverTool?.execute) { + progress = snapshot.resume({ + exception: { + type: 'ValueError', + message: `Unknown function: ${snapshot.functionName}`, + }, + }); + continue; + } + + try { + const result = await serverTool.execute( + snapshot.kwargs as Record + ); + progress = snapshot.resume({ returnValue: result }); + } catch (error) { + progress = snapshot.resume({ + exception: { + type: 'RuntimeError', + message: error instanceof Error ? error.message : String(error), + }, + }); + } + } + + return montyToJson((progress as MontyComplete).output); + } catch (error) { + if (error instanceof MontySyntaxError) { + return `Syntax Error: ${error.message}`; + } + if (error instanceof MontyRuntimeError) { + return `Runtime Error: ${error.message}\n${error.traceback()}`; + } + if (error instanceof MontyTypingError) { + return `Type Error: ${error.displayDiagnostics()}`; + } + return `Error: ${error instanceof Error ? error.message : String(error)}`; + } + }); + + return [searchTool, executeTool]; +} + +// ─── createTools ───────────────────────────────────────────────────────────── + /** * Create TanStack AI server tools from an OpenAPI specification. * @@ -38,14 +277,17 @@ export interface Spec2ToolsOptions { * return toServerSentEventsResponse(stream); * ``` * - * @throws Error if the API requires authentication + * @throws Error if the API requires authentication (unless codeMode is enabled) */ -export async function createTools(options: Spec2ToolsOptions): Promise { +export async function createTools( + options: Spec2ToolsOptions +): Promise { const spec = await loadOpenAPISpec(options.spec); const baseUrl = extractBaseUrl(spec); const authConfig = extractAuthConfig(spec); - if (authConfig.type !== 'none') { + // Check if auth is required (skip in code mode — auth is handled internally) + if (!options.codeMode && authConfig.type !== 'none') { throw new Error( `This API requires authentication (${authConfig.type}). ` + `The createTools() function only supports APIs without authentication. ` + @@ -57,7 +299,7 @@ export async function createTools(options: Spec2ToolsOptions): Promise { + const serverTools = tools.map((t) => { const inputSchema = zodToJsonSchema(t.parameters) as JSONSchema; return toolDefinition({ name: t.name, @@ -67,4 +309,10 @@ export async function createTools(options: Spec2ToolsOptions): Promise Date: Thu, 12 Mar 2026 22:56:57 +0000 Subject: [PATCH 03/11] 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 --- examples/sdk-tanstack-node/.env.example | 1 + examples/sdk-tanstack-node/README.md | 33 ++++++ examples/sdk-tanstack-node/index.ts | 25 +++++ examples/sdk-tanstack-node/package.json | 22 ++++ examples/sdk-tanstack-node/tsconfig.json | 13 +++ pnpm-lock.yaml | 135 ++++++++++++++++++++--- pnpm-workspace.yaml | 1 + 7 files changed, 216 insertions(+), 14 deletions(-) create mode 100644 examples/sdk-tanstack-node/.env.example create mode 100644 examples/sdk-tanstack-node/README.md create mode 100644 examples/sdk-tanstack-node/index.ts create mode 100644 examples/sdk-tanstack-node/package.json create mode 100644 examples/sdk-tanstack-node/tsconfig.json diff --git a/examples/sdk-tanstack-node/.env.example b/examples/sdk-tanstack-node/.env.example new file mode 100644 index 0000000..ed6ed73 --- /dev/null +++ b/examples/sdk-tanstack-node/.env.example @@ -0,0 +1 @@ +OPENAI_API_KEY=sk-... diff --git a/examples/sdk-tanstack-node/README.md b/examples/sdk-tanstack-node/README.md new file mode 100644 index 0000000..af57682 --- /dev/null +++ b/examples/sdk-tanstack-node/README.md @@ -0,0 +1,33 @@ +# example: sdk-tanstack-node + +A minimal Node.js script that generates [TanStack AI](https://tanstack.com/ai) tools from an OpenAPI spec using `@spec2tools/sdk-tanstack`, then runs 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 +``` + +## Run + +```bash +# default prompt +pnpm start + +# custom prompt +pnpm start "How many posts does user 1 have?" +``` + +## How it works + +1. `createTools({ spec })` reads the OpenAPI spec and returns a `ServerTool[]` +2. The tools are passed to TanStack AI's `chat()` together with the OpenAI adapter +3. The model calls the relevant API endpoints autonomously to answer the prompt +4. `streamToText()` collects the streamed response into a final string diff --git a/examples/sdk-tanstack-node/index.ts b/examples/sdk-tanstack-node/index.ts new file mode 100644 index 0000000..d856582 --- /dev/null +++ b/examples/sdk-tanstack-node/index.ts @@ -0,0 +1,25 @@ +import { chat, streamToText } 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, +}); + +const text = await streamToText(stream); +console.log(text); diff --git a/examples/sdk-tanstack-node/package.json b/examples/sdk-tanstack-node/package.json new file mode 100644 index 0000000..2689caf --- /dev/null +++ b/examples/sdk-tanstack-node/package.json @@ -0,0 +1,22 @@ +{ + "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 index.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" + } +} diff --git a/examples/sdk-tanstack-node/tsconfig.json b/examples/sdk-tanstack-node/tsconfig.json new file mode 100644 index 0000000..b9573c5 --- /dev/null +++ b/examples/sdk-tanstack-node/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true + }, + "include": ["*.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0338b72..48cc481 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,17 +12,42 @@ importers: specifier: ^2.27.0 version: 2.29.8(@types/node@22.19.7) + examples/sdk-tanstack-node: + dependencies: + '@spec2tools/sdk-tanstack': + specifier: workspace:* + version: link:../../packages/sdk-tanstack + '@tanstack/ai': + specifier: ^0.6.3 + version: 0.6.3 + '@tanstack/ai-openai': + specifier: ^0.6.0 + version: 0.6.0(@tanstack/ai@0.6.3)(zod@4.3.6) + zod: + specifier: ^4.0.0 + version: 4.3.6 + devDependencies: + '@types/node': + specifier: ^22.0.0 + version: 22.19.7 + tsx: + specifier: ^4.0.0 + version: 4.21.0 + typescript: + specifier: ^5.6.0 + version: 5.9.3 + packages/cli: dependencies: '@ai-sdk/openai': specifier: ^3.0.25 - version: 3.0.25(zod@3.25.76) + version: 3.0.25(zod@4.3.6) '@spec2tools/core': specifier: workspace:* version: link:../core ai: specifier: ^6.0.68 - version: 6.0.68(zod@3.25.76) + version: 6.0.68(zod@4.3.6) chalk: specifier: ^5.3.0 version: 5.6.2 @@ -78,7 +103,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.7)(yaml@2.8.2) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.7)(tsx@4.21.0)(yaml@2.8.2) packages/sdk: dependencies: @@ -100,7 +125,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.7)(yaml@2.8.2) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.7)(tsx@4.21.0)(yaml@2.8.2) packages/sdk-tanstack: dependencies: @@ -128,7 +153,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.7)(yaml@2.8.2) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.7)(tsx@4.21.0)(yaml@2.8.2) packages/stdio-mcp: dependencies: @@ -642,6 +667,12 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@tanstack/ai-openai@0.6.0': + resolution: {integrity: sha512-CSDgYr63ftw+nGb1IpB+3qIOngLE1pZjasiaeOdDQ6CbSEJDNcR7VNmyTx3RsoTh9fChKbLhJzYpem1Mpag2og==} + peerDependencies: + '@tanstack/ai': ^0.6.1 + zod: ^4.0.0 + '@tanstack/ai@0.6.3': resolution: {integrity: sha512-uCuP27PScfJXAKS/laqinlVSFSzPc2RMgXj0d7S7f6aqoIP/ack/0t5HjLdFgRMEOJ1wxbYmUV5Ln94wYg5t8Q==} engines: {node: '>=18'} @@ -1040,6 +1071,9 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + get-tsconfig@4.13.6: + resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1253,6 +1287,18 @@ packages: resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} engines: {node: '>=18'} + openai@6.27.0: + resolution: {integrity: sha512-osTKySlrdYrLYTt0zjhY8yp0JUBmWDCN+Q+QxsV4xMQnnoVFpylgKGgxwN8sSdTNw0G4y+WUXs4eCMWpyDNWZQ==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + ora@8.2.0: resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} engines: {node: '>=18'} @@ -1370,6 +1416,9 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + restore-cursor@5.1.0: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} @@ -1518,6 +1567,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + type-is@2.0.1: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} @@ -1646,6 +1700,9 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + snapshots: '@ai-sdk/gateway@3.0.32(zod@3.25.76)': @@ -1655,6 +1712,13 @@ snapshots: '@vercel/oidc': 3.1.0 zod: 3.25.76 + '@ai-sdk/gateway@3.0.32(zod@4.3.6)': + dependencies: + '@ai-sdk/provider': 3.0.7 + '@ai-sdk/provider-utils': 4.0.13(zod@4.3.6) + '@vercel/oidc': 3.1.0 + zod: 4.3.6 + '@ai-sdk/mcp@1.0.21(zod@3.25.76)': dependencies: '@ai-sdk/provider': 3.0.8 @@ -1662,11 +1726,11 @@ snapshots: pkce-challenge: 5.0.1 zod: 3.25.76 - '@ai-sdk/openai@3.0.25(zod@3.25.76)': + '@ai-sdk/openai@3.0.25(zod@4.3.6)': dependencies: '@ai-sdk/provider': 3.0.7 - '@ai-sdk/provider-utils': 4.0.13(zod@3.25.76) - zod: 3.25.76 + '@ai-sdk/provider-utils': 4.0.13(zod@4.3.6) + zod: 4.3.6 '@ai-sdk/provider-utils@4.0.13(zod@3.25.76)': dependencies: @@ -1675,6 +1739,13 @@ snapshots: eventsource-parser: 3.0.6 zod: 3.25.76 + '@ai-sdk/provider-utils@4.0.13(zod@4.3.6)': + dependencies: + '@ai-sdk/provider': 3.0.7 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.0.6 + zod: 4.3.6 + '@ai-sdk/provider-utils@4.0.15(zod@3.25.76)': dependencies: '@ai-sdk/provider': 3.0.8 @@ -2108,6 +2179,14 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@tanstack/ai-openai@0.6.0(@tanstack/ai@0.6.3)(zod@4.3.6)': + dependencies: + '@tanstack/ai': 0.6.3 + openai: 6.27.0(zod@4.3.6) + zod: 4.3.6 + transitivePeerDependencies: + - ws + '@tanstack/ai@0.6.3': dependencies: '@tanstack/devtools-event-client': 0.4.3 @@ -2183,13 +2262,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@22.19.7)(yaml@2.8.2))': + '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@22.19.7)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@22.19.7)(yaml@2.8.2) + vite: 7.3.1(@types/node@22.19.7)(tsx@4.21.0)(yaml@2.8.2) '@vitest/pretty-format@4.0.18': dependencies: @@ -2226,6 +2305,14 @@ snapshots: '@opentelemetry/api': 1.9.0 zod: 3.25.76 + ai@6.0.68(zod@4.3.6): + dependencies: + '@ai-sdk/gateway': 3.0.32(zod@4.3.6) + '@ai-sdk/provider': 3.0.7 + '@ai-sdk/provider-utils': 4.0.13(zod@4.3.6) + '@opentelemetry/api': 1.9.0 + zod: 4.3.6 + ajv-formats@3.0.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 @@ -2543,6 +2630,10 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 + get-tsconfig@4.13.6: + dependencies: + resolve-pkg-maps: 1.0.0 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -2712,6 +2803,10 @@ snapshots: is-inside-container: 1.0.0 wsl-utils: 0.1.0 + openai@6.27.0(zod@4.3.6): + optionalDependencies: + zod: 4.3.6 + ora@8.2.0: dependencies: chalk: 5.6.2 @@ -2811,6 +2906,8 @@ snapshots: resolve-from@5.0.0: {} + resolve-pkg-maps@1.0.0: {} + restore-cursor@5.1.0: dependencies: onetime: 7.0.0 @@ -2991,6 +3088,13 @@ snapshots: tslib@2.8.1: optional: true + tsx@4.21.0: + dependencies: + esbuild: 0.27.3 + get-tsconfig: 4.13.6 + optionalDependencies: + fsevents: 2.3.3 + type-is@2.0.1: dependencies: content-type: 1.0.5 @@ -3007,7 +3111,7 @@ snapshots: vary@1.1.2: {} - vite@7.3.1(@types/node@22.19.7)(yaml@2.8.2): + vite@7.3.1(@types/node@22.19.7)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -3018,12 +3122,13 @@ snapshots: optionalDependencies: '@types/node': 22.19.7 fsevents: 2.3.3 + tsx: 4.21.0 yaml: 2.8.2 - vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.7)(yaml@2.8.2): + vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.7)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@22.19.7)(yaml@2.8.2)) + '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@22.19.7)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -3040,7 +3145,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@22.19.7)(yaml@2.8.2) + vite: 7.3.1(@types/node@22.19.7)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 @@ -3080,3 +3185,5 @@ snapshots: zod: 3.25.76 zod@3.25.76: {} + + zod@4.3.6: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index dee51e9..89723f9 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,3 @@ packages: - "packages/*" + - "examples/*" From 2ffdf6b7307fddc1ec61047fba7ce5a247e80463 Mon Sep 17 00:00:00 2001 From: CahidArda Date: Fri, 13 Mar 2026 02:07:36 +0300 Subject: [PATCH 04/11] refactor: streamline chat logging and remove unused streamToText import --- examples/sdk-tanstack-node/index.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/examples/sdk-tanstack-node/index.ts b/examples/sdk-tanstack-node/index.ts index d856582..5af880c 100644 --- a/examples/sdk-tanstack-node/index.ts +++ b/examples/sdk-tanstack-node/index.ts @@ -1,4 +1,4 @@ -import { chat, streamToText } from '@tanstack/ai'; +import { chat } from '@tanstack/ai'; import { openaiText } from '@tanstack/ai-openai'; import { createTools } from '@spec2tools/sdk-tanstack'; import path from 'path'; @@ -11,7 +11,7 @@ const SPEC = path.resolve(__dirname, '../../packages/cli/examples/sample-api.yam const prompt = process.argv[2] ?? 'List the first 3 users and tell me their names.'; -console.log(`Prompt: ${prompt}\n`); +console.log(` > Prompt: ${prompt}\n`); const tools = await createTools({ spec: SPEC }); @@ -21,5 +21,12 @@ const stream = chat({ tools, }); -const text = await streamToText(stream); -console.log(text); +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 ended: ${chunk.toolName}\n`); + } else if (chunk.type === "TEXT_MESSAGE_CONTENT") { + process.stdout.write(chunk.delta); // Stream text content to console + } +} From 8f8afb47bd62a32073ae8314965eeedf8e1d5d22 Mon Sep 17 00:00:00 2001 From: CahidArda Date: Fri, 13 Mar 2026 02:11:02 +0300 Subject: [PATCH 05/11] fix: print input in tool call end --- examples/sdk-tanstack-node/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/sdk-tanstack-node/index.ts b/examples/sdk-tanstack-node/index.ts index 5af880c..4472ae6 100644 --- a/examples/sdk-tanstack-node/index.ts +++ b/examples/sdk-tanstack-node/index.ts @@ -25,7 +25,7 @@ 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 ended: ${chunk.toolName}\n`); + 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 } From 95da449c26d887283a80cc061df930a69bcc674d Mon Sep 17 00:00:00 2001 From: Upstash Box Date: Thu, 12 Mar 2026 23:14:05 +0000 Subject: [PATCH 06/11] 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 --- examples/sdk-tanstack-node/README.md | 53 +++++++++++++--- examples/sdk-tanstack-node/code-mode.ts | 36 +++++++++++ .../sdk-tanstack-node/convert-code-mode.ts | 63 +++++++++++++++++++ examples/sdk-tanstack-node/package.json | 2 + 4 files changed, 144 insertions(+), 10 deletions(-) create mode 100644 examples/sdk-tanstack-node/code-mode.ts create mode 100644 examples/sdk-tanstack-node/convert-code-mode.ts diff --git a/examples/sdk-tanstack-node/README.md b/examples/sdk-tanstack-node/README.md index af57682..a496158 100644 --- a/examples/sdk-tanstack-node/README.md +++ b/examples/sdk-tanstack-node/README.md @@ -1,6 +1,6 @@ # example: sdk-tanstack-node -A minimal Node.js script that generates [TanStack AI](https://tanstack.com/ai) tools from an OpenAPI spec using `@spec2tools/sdk-tanstack`, then runs a chat with OpenAI. +Node.js examples that use `@spec2tools/sdk-tanstack` to generate [TanStack AI](https://tanstack.com/ai) tools, then run a chat with OpenAI. ## Setup @@ -15,19 +15,52 @@ Install dependencies from the repo root: pnpm install ``` -## Run +--- + +## Examples + +### 1. `index.ts` — one tool per endpoint + +Generates one `ServerTool` per OpenAPI operation and passes them all to `chat()`. ```bash -# default prompt pnpm start - -# custom prompt pnpm start "How many posts does user 1 have?" ``` -## How it works +**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. `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. `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." +``` -1. `createTools({ spec })` reads the OpenAPI spec and returns a `ServerTool[]` -2. The tools are passed to TanStack AI's `chat()` together with the OpenAI adapter -3. The model calls the relevant API endpoints autonomously to answer the prompt -4. `streamToText()` collects the streamed response into a final string +**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 diff --git a/examples/sdk-tanstack-node/code-mode.ts b/examples/sdk-tanstack-node/code-mode.ts new file mode 100644 index 0000000..f8dabac --- /dev/null +++ b/examples/sdk-tanstack-node/code-mode.ts @@ -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); + } +} diff --git a/examples/sdk-tanstack-node/convert-code-mode.ts b/examples/sdk-tanstack-node/convert-code-mode.ts new file mode 100644 index 0000000..07651ea --- /dev/null +++ b/examples/sdk-tanstack-node/convert-code-mode.ts @@ -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); + } +} diff --git a/examples/sdk-tanstack-node/package.json b/examples/sdk-tanstack-node/package.json index 2689caf..b9435cc 100644 --- a/examples/sdk-tanstack-node/package.json +++ b/examples/sdk-tanstack-node/package.json @@ -6,6 +6,8 @@ "type": "module", "scripts": { "start": "tsx index.ts", + "start:code-mode": "tsx code-mode.ts", + "start:convert": "tsx convert-code-mode.ts", "typecheck": "tsc --noEmit" }, "dependencies": { From e1a48cbd672f6dcaad4bf0822a1b89b621b6b8cc Mon Sep 17 00:00:00 2001 From: Upstash Box Date: Thu, 12 Mar 2026 23:15:09 +0000 Subject: [PATCH 07/11] refactor(examples/sdk-tanstack-node): prefix example files with example- MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- examples/sdk-tanstack-node/README.md | 6 +++--- examples/sdk-tanstack-node/{index.ts => example-basic.ts} | 0 .../{code-mode.ts => example-code-mode.ts} | 0 .../{convert-code-mode.ts => example-convert-code-mode.ts} | 0 examples/sdk-tanstack-node/package.json | 6 +++--- 5 files changed, 6 insertions(+), 6 deletions(-) rename examples/sdk-tanstack-node/{index.ts => example-basic.ts} (100%) rename examples/sdk-tanstack-node/{code-mode.ts => example-code-mode.ts} (100%) rename examples/sdk-tanstack-node/{convert-code-mode.ts => example-convert-code-mode.ts} (100%) diff --git a/examples/sdk-tanstack-node/README.md b/examples/sdk-tanstack-node/README.md index a496158..e805aef 100644 --- a/examples/sdk-tanstack-node/README.md +++ b/examples/sdk-tanstack-node/README.md @@ -19,7 +19,7 @@ pnpm install ## Examples -### 1. `index.ts` — one tool per endpoint +### 1. `example-basic.ts` — one tool per endpoint Generates one `ServerTool` per OpenAPI operation and passes them all to `chat()`. @@ -35,7 +35,7 @@ pnpm start "How many posts does user 1 have?" --- -### 2. `code-mode.ts` — collapsed into 2 tools via `codeMode` +### 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. @@ -51,7 +51,7 @@ pnpm start:code-mode "How many posts does user 1 have?" --- -### 3. `convert-code-mode.ts` — `convertToolsToCodeMode` with hand-written tools +### 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. diff --git a/examples/sdk-tanstack-node/index.ts b/examples/sdk-tanstack-node/example-basic.ts similarity index 100% rename from examples/sdk-tanstack-node/index.ts rename to examples/sdk-tanstack-node/example-basic.ts diff --git a/examples/sdk-tanstack-node/code-mode.ts b/examples/sdk-tanstack-node/example-code-mode.ts similarity index 100% rename from examples/sdk-tanstack-node/code-mode.ts rename to examples/sdk-tanstack-node/example-code-mode.ts diff --git a/examples/sdk-tanstack-node/convert-code-mode.ts b/examples/sdk-tanstack-node/example-convert-code-mode.ts similarity index 100% rename from examples/sdk-tanstack-node/convert-code-mode.ts rename to examples/sdk-tanstack-node/example-convert-code-mode.ts diff --git a/examples/sdk-tanstack-node/package.json b/examples/sdk-tanstack-node/package.json index b9435cc..47f05a9 100644 --- a/examples/sdk-tanstack-node/package.json +++ b/examples/sdk-tanstack-node/package.json @@ -5,9 +5,9 @@ "description": "Node.js example: generate TanStack AI tools from an OpenAPI spec", "type": "module", "scripts": { - "start": "tsx index.ts", - "start:code-mode": "tsx code-mode.ts", - "start:convert": "tsx convert-code-mode.ts", + "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": { From e2e0afac775fcc9b2d9ef2ac1dcddaed153f4fe0 Mon Sep 17 00:00:00 2001 From: CahidArda Date: Fri, 13 Mar 2026 02:44:01 +0300 Subject: [PATCH 08/11] 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 --- packages/core/package.json | 4 +++- packages/core/src/ai-tools.test.ts | 2 +- packages/core/src/code-mode.test.ts | 2 +- packages/core/src/code-mode.ts | 2 +- packages/core/src/openapi-parser.ts | 2 +- packages/core/src/tool-executor.ts | 2 +- packages/core/src/types.ts | 2 +- packages/sdk-tanstack/package.json | 4 +--- packages/sdk-tanstack/src/lib.ts | 2 +- packages/sdk/package.json | 5 ++--- packages/stdio-mcp/src/server.ts | 5 +++-- pnpm-lock.yaml | 24 ++++++++++++++---------- 12 files changed, 30 insertions(+), 26 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 798b989..1d059e0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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", diff --git a/packages/core/src/ai-tools.test.ts b/packages/core/src/ai-tools.test.ts index 72a522c..8dc2dd6 100644 --- a/packages/core/src/ai-tools.test.ts +++ b/packages/core/src/ai-tools.test.ts @@ -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'; diff --git a/packages/core/src/code-mode.test.ts b/packages/core/src/code-mode.test.ts index 7bb7770..90209fa 100644 --- a/packages/core/src/code-mode.test.ts +++ b/packages/core/src/code-mode.test.ts @@ -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'; diff --git a/packages/core/src/code-mode.ts b/packages/core/src/code-mode.ts index 5130e20..5687e53 100644 --- a/packages/core/src/code-mode.ts +++ b/packages/core/src/code-mode.ts @@ -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, diff --git a/packages/core/src/openapi-parser.ts b/packages/core/src/openapi-parser.ts index f84a322..305c498 100644 --- a/packages/core/src/openapi-parser.ts +++ b/packages/core/src/openapi-parser.ts @@ -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 { diff --git a/packages/core/src/tool-executor.ts b/packages/core/src/tool-executor.ts index b89cec3..a55399e 100644 --- a/packages/core/src/tool-executor.ts +++ b/packages/core/src/tool-executor.ts @@ -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'; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 281fee0..ce70495 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1,4 +1,4 @@ -import { z } from 'zod'; +import { z } from 'zod/v3'; export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; diff --git a/packages/sdk-tanstack/package.json b/packages/sdk-tanstack/package.json index 4585911..9cb5cb6 100644 --- a/packages/sdk-tanstack/package.json +++ b/packages/sdk-tanstack/package.json @@ -39,13 +39,11 @@ "dependencies": { "@pydantic/monty": "^0.0.7", "@spec2tools/core": "workspace:*", - "@tanstack/ai": "^0.6.3", - "zod": "^3.24.0", "zod-to-json-schema": "^3.24.0" }, "peerDependencies": { "@tanstack/ai": "^0.6.3", - "zod": "^3.24.0" + "zod": "^3.25.0 || ^4.0.0" }, "devDependencies": { "@types/node": "^22.0.0", diff --git a/packages/sdk-tanstack/src/lib.ts b/packages/sdk-tanstack/src/lib.ts index 8453a1f..71a3d7e 100644 --- a/packages/sdk-tanstack/src/lib.ts +++ b/packages/sdk-tanstack/src/lib.ts @@ -8,7 +8,7 @@ import { } from '@spec2tools/core'; import { toolDefinition, type ServerTool, type JSONSchema } from '@tanstack/ai'; import { zodToJsonSchema } from 'zod-to-json-schema'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { Monty, MontySnapshot, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 5c0891b..71e9028 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -38,11 +38,10 @@ }, "dependencies": { "@spec2tools/core": "workspace:*", - "ai": "^6.0.68", - "zod": "^3.24.0" + "ai": "^6.0.68" }, "peerDependencies": { - "zod": "^3.24.0" + "zod": "^3.25.0 || ^4.0.0" }, "devDependencies": { "@types/node": "^22.0.0", diff --git a/packages/stdio-mcp/src/server.ts b/packages/stdio-mcp/src/server.ts index 92c93a6..d8a465f 100644 --- a/packages/stdio-mcp/src/server.ts +++ b/packages/stdio-mcp/src/server.ts @@ -63,7 +63,8 @@ export async function startMcpServer(options: McpServerOptions): Promise { : { type: 'sse' as const, url: options.sseUrl!, headers: { Authorization: `Bearer ${options.apiKey || ''}` } }; mcpClient = await createMCPClient({ transport }); - tools = await mcpClient.tools() as ToolSet; + // TODO: remove the double cast. it was added to fix a "Type instantiation is excessively deep and possibly infinite" error + tools = await mcpClient.tools() as unknown as ToolSet; if (options.codeMode) { tools = toCodeModeTools(tools); @@ -107,7 +108,7 @@ export async function startMcpServer(options: McpServerOptions): Promise { if ('jsonSchema' in schemaObj && schemaObj.jsonSchema) { inputSchema = schemaObj.jsonSchema as Record; } else if ('_def' in schemaObj) { - inputSchema = zodToJsonSchema(schema as import('zod').ZodType, { target: 'jsonSchema7' }) as Record; + inputSchema = zodToJsonSchema(schema as import('zod/v3').ZodType, { target: 'jsonSchema7' }) as Record; } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 48cc481..ae0704f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,7 +75,7 @@ importers: version: 0.0.7 ai: specifier: ^6.0.68 - version: 6.0.68(zod@3.25.76) + version: 6.0.68(zod@4.3.6) express: specifier: ^5.0.0 version: 5.2.1 @@ -86,11 +86,11 @@ importers: specifier: ^2.5.0 version: 2.8.2 zod: - specifier: ^3.24.0 - version: 3.25.76 + specifier: ^3.25.0 || ^4.0.0 + version: 4.3.6 zod-to-json-schema: specifier: ^3.24.0 - version: 3.25.1(zod@3.25.76) + version: 3.25.1(zod@4.3.6) devDependencies: '@types/express': specifier: ^5.0.0 @@ -112,10 +112,10 @@ importers: version: link:../core ai: specifier: ^6.0.68 - version: 6.0.68(zod@3.25.76) + version: 6.0.68(zod@4.3.6) zod: - specifier: ^3.24.0 - version: 3.25.76 + specifier: ^3.25.0 || ^4.0.0 + version: 4.3.6 devDependencies: '@types/node': specifier: ^22.0.0 @@ -139,11 +139,11 @@ importers: specifier: ^0.6.3 version: 0.6.3 zod: - specifier: ^3.24.0 - version: 3.25.76 + specifier: ^3.25.0 || ^4.0.0 + version: 4.3.6 zod-to-json-schema: specifier: ^3.24.0 - version: 3.25.1(zod@3.25.76) + version: 3.25.1(zod@4.3.6) devDependencies: '@types/node': specifier: ^22.0.0 @@ -3184,6 +3184,10 @@ snapshots: dependencies: zod: 3.25.76 + zod-to-json-schema@3.25.1(zod@4.3.6): + dependencies: + zod: 4.3.6 + zod@3.25.76: {} zod@4.3.6: {} From 235f0fc0d2b6854cb7552bcdcec1d057cc7d12ca Mon Sep 17 00:00:00 2001 From: CahidArda Date: Fri, 13 Mar 2026 02:52:56 +0300 Subject: [PATCH 09/11] ci: add changeset and CI workflows Co-Authored-By: Claude Opus 4.6 --- .github/workflows/changeset.yml | 39 +++++++++++++++++++++++++++++++++ .github/workflows/ci.yml | 34 ++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 .github/workflows/changeset.yml create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/changeset.yml b/.github/workflows/changeset.yml new file mode 100644 index 0000000..26df777 --- /dev/null +++ b/.github/workflows/changeset.yml @@ -0,0 +1,39 @@ +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 + with: + version: 9 + + - 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 }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a667699 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +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 + with: + version: 9 + + - 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 From 4218f281d076d3511648202460c779bae31fe6da Mon Sep 17 00:00:00 2001 From: CahidArda Date: Fri, 13 Mar 2026 02:55:30 +0300 Subject: [PATCH 10/11] chore: add changesets --- .changeset/plain-doodles-live.md | 5 +++++ .changeset/thirty-sheep-grow.md | 7 +++++++ 2 files changed, 12 insertions(+) create mode 100644 .changeset/plain-doodles-live.md create mode 100644 .changeset/thirty-sheep-grow.md diff --git a/.changeset/plain-doodles-live.md b/.changeset/plain-doodles-live.md new file mode 100644 index 0000000..ad32afe --- /dev/null +++ b/.changeset/plain-doodles-live.md @@ -0,0 +1,5 @@ +--- +"@spec2tools/sdk-tanstack": patch +--- + +Add sdk-tanstack package diff --git a/.changeset/thirty-sheep-grow.md b/.changeset/thirty-sheep-grow.md new file mode 100644 index 0000000..7376f40 --- /dev/null +++ b/.changeset/thirty-sheep-grow.md @@ -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 From 527073240f4180f8c2ce0f282e15a44a99e99068 Mon Sep 17 00:00:00 2001 From: CahidArda Date: Fri, 13 Mar 2026 02:56:22 +0300 Subject: [PATCH 11/11] fix(ci): remove explicit pnpm version to use packageManager field Co-Authored-By: Claude Opus 4.6 --- .github/workflows/changeset.yml | 2 -- .github/workflows/ci.yml | 2 -- 2 files changed, 4 deletions(-) diff --git a/.github/workflows/changeset.yml b/.github/workflows/changeset.yml index 26df777..86e5269 100644 --- a/.github/workflows/changeset.yml +++ b/.github/workflows/changeset.yml @@ -19,8 +19,6 @@ jobs: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - with: - version: 9 - uses: actions/setup-node@v4 with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a667699..11612aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,8 +17,6 @@ jobs: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - with: - version: 9 - uses: actions/setup-node@v4 with: