Skip to content

Commit 334d5b2

Browse files
authored
feat: add code mode basics with pydantic monty (#11)
* fix: add code mode basics with pydantic monty * feat: add tests, fix issues, update versions * fix: add option to connect to existing mcp servers * fix: bump cli
1 parent 9c5dd39 commit 334d5b2

29 files changed

Lines changed: 2190 additions & 195 deletions

README.md

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,38 @@ export OPENAI_API_KEY=your-api-key
4444
npx @spec2tools/cli start --spec https://api.example.com/openapi.json
4545
```
4646

47-
### Using as MCP Server (with Claude Desktop)
48-
49-
Add to your `claude_desktop_config.json`:
50-
51-
```json
52-
{
53-
"mcpServers": {
54-
"my-api": {
55-
"command": "npx",
56-
"args": ["@spec2tools/stdio-mcp", "./path/to/openapi.yaml"]
57-
}
58-
}
59-
}
47+
### Using as MCP Server
48+
49+
```bash
50+
# Add to Claude Code (or any MCP client)
51+
claude mcp add --transport stdio my-api \
52+
-- npx @spec2tools/stdio-mcp ./path/to/openapi.yaml
53+
```
54+
55+
### Code Mode
56+
57+
Collapse all API endpoints into just 2 tools (`search` + `execute`), cutting input token usage by ~99.9%:
58+
59+
```bash
60+
# MCP server with code mode
61+
claude mcp add --transport stdio my-api \
62+
-- npx @spec2tools/stdio-mcp ./openapi.yaml --code-mode
63+
```
64+
65+
```ts
66+
// SDK with code mode
67+
const tools = await createTools({
68+
spec: './openapi.yaml',
69+
codeMode: true,
70+
});
71+
```
72+
73+
You can also convert any existing AI SDK tools to code mode:
74+
75+
```ts
76+
import { toCodeModeTools } from '@spec2tools/core';
77+
78+
const codeModeTools = toCodeModeTools(existingTools);
6079
```
6180

6281
## Thesis

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"scripts": {
88
"build": "pnpm -r run build",
99
"dev": "pnpm -r run dev",
10+
"test": "pnpm -r run test",
1011
"typecheck": "pnpm -r run typecheck",
1112
"clean": "pnpm -r exec rm -rf dist node_modules",
1213
"changeset": "changeset",

packages/cli/CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
# @spec2tools/cli
22

3+
## 0.2.1
4+
5+
### Patch Changes
6+
7+
- Re-release
8+
9+
## 0.2.0
10+
11+
### Minor Changes
12+
13+
- Add code mode MCP option
14+
15+
### Patch Changes
16+
17+
- Updated dependencies
18+
- @spec2tools/core@0.2.0
19+
320
## 0.1.2
421

522
### Patch Changes

packages/cli/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ npx @spec2tools/cli start --spec ./openapi.yaml --no-auth
3838

3939
# Provide API key directly
4040
npx @spec2tools/cli start --spec ./openapi.yaml --api-key "your-api-key"
41+
42+
# Enable code mode (2 tools: search + execute)
43+
npx @spec2tools/cli start --spec ./openapi.yaml --code-mode
4144
```
4245

4346
### Chat Mode

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@spec2tools/cli",
3-
"version": "0.1.2",
3+
"version": "0.2.1",
44
"description": "CLI for interacting with OpenAPI-based AI tools",
55
"type": "module",
66
"main": "dist/index.js",

packages/cli/src/agent.ts

Lines changed: 39 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { generateText, tool, stepCountIs, ModelMessage } from 'ai';
1+
import { generateText, stepCountIs, ModelMessage } from 'ai';
22
import { openai } from '@ai-sdk/openai';
3-
import { type Tool, ToolExecutionError } from '@spec2tools/core';
3+
import { type ToolSet, ToolExecutionError } from '@spec2tools/core';
44
import chalk from 'chalk';
55

66
const MAX_OUTPUT_LENGTH = 500;
@@ -17,7 +17,7 @@ function trimOutput(value: unknown): string {
1717
}
1818

1919
interface AgentConfig {
20-
tools: Tool[];
20+
tools: ToolSet;
2121
model?: string;
2222
maxSteps?: number;
2323
}
@@ -26,7 +26,7 @@ interface AgentConfig {
2626
* AI Agent that uses OpenAPI tools
2727
*/
2828
export class Agent {
29-
private tools: Tool[];
29+
private tools: ToolSet;
3030
private model: string;
3131
private maxSteps: number;
3232
private conversationHistory: ModelMessage[];
@@ -42,12 +42,15 @@ export class Agent {
4242
* Get available tools description for the agent
4343
*/
4444
getToolsDescription(): string {
45-
if (this.tools.length === 0) {
45+
const names = Object.keys(this.tools);
46+
if (names.length === 0) {
4647
return 'No tools available.';
4748
}
4849

49-
const toolDescriptions = this.tools.map((tool) => {
50-
return `- ${tool.name}: ${tool.description}`;
50+
const toolDescriptions = names.map((name) => {
51+
const t = this.tools[name];
52+
const desc = 'description' in t ? t.description : '';
53+
return `- ${name}: ${desc}`;
5154
});
5255

5356
return `I have access to the following tools:\n${toolDescriptions.join('\n')}`;
@@ -64,36 +67,32 @@ export class Agent {
6467
});
6568

6669
try {
67-
// Build AI SDK tools from our tool definitions
68-
const aiTools: Parameters<typeof generateText>[0]['tools'] = {};
69-
70-
for (const t of this.tools) {
71-
const toolExecute = t.execute;
72-
const toolName = t.name;
73-
74-
aiTools![t.name] = tool({
75-
description: t.description,
76-
inputSchema: t.parameters,
77-
execute: async (params) => {
78-
console.log(
79-
chalk.dim(`\n[Calling ${toolName} with ${JSON.stringify(params)}]`)
80-
);
81-
82-
try {
83-
const result = await toolExecute(params);
84-
console.log(
85-
chalk.dim(`[${toolName} returned: ${trimOutput(result)}]\n`)
86-
);
87-
return result;
88-
} catch (error) {
89-
if (error instanceof ToolExecutionError) {
90-
console.log(chalk.red(`[${toolName} failed: ${error.message}]\n`));
91-
throw error;
70+
// Wrap each tool to add CLI logging
71+
const wrappedTools: ToolSet = {};
72+
for (const [name, t] of Object.entries(this.tools)) {
73+
const originalExecute = 'execute' in t ? t.execute : undefined;
74+
wrappedTools[name] = {
75+
...t,
76+
execute: originalExecute
77+
? async (params: unknown, options: unknown) => {
78+
console.log(
79+
chalk.dim(`\n[Calling ${name} with ${JSON.stringify(params)}]`)
80+
);
81+
try {
82+
const result = await (originalExecute as Function)(params, options);
83+
console.log(
84+
chalk.dim(`[${name} returned: ${trimOutput(result)}]\n`)
85+
);
86+
return result;
87+
} catch (error) {
88+
if (error instanceof ToolExecutionError) {
89+
console.log(chalk.red(`[${name} failed: ${error.message}]\n`));
90+
}
91+
throw error;
92+
}
9293
}
93-
throw error;
94-
}
95-
},
96-
});
94+
: undefined,
95+
} as ToolSet[string];
9796
}
9897

9998
// Build system prompt
@@ -107,7 +106,7 @@ If a tool call fails, explain the error to the user.`;
107106
model: openai(this.model),
108107
system: systemPrompt,
109108
messages: this.conversationHistory,
110-
tools: aiTools,
109+
tools: wrappedTools,
111110
stopWhen: stepCountIs(this.maxSteps),
112111
});
113112

@@ -143,13 +142,13 @@ If a tool call fails, explain the error to the user.`;
143142
* Get the list of tool names
144143
*/
145144
getToolNames(): string[] {
146-
return this.tools.map((t) => t.name);
145+
return Object.keys(this.tools);
147146
}
148147

149148
/**
150149
* Get a specific tool by name
151150
*/
152-
getTool(name: string): Tool | undefined {
153-
return this.tools.find((t) => t.name === name);
151+
getTool(name: string): ToolSet[string] | undefined {
152+
return this.tools[name];
154153
}
155154
}

packages/cli/src/cli.ts

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@ import {
1212
AuthManager,
1313
createExecutableTools,
1414
executeToolByName,
15+
toAISDKTools,
16+
toCodeModeTools,
1517
UnsupportedSchemaError,
1618
AuthenticationError,
1719
ToolExecutionError,
1820
SpecLoadError,
1921
type Session,
2022
type Tool,
23+
type ToolSet,
2124
} from '@spec2tools/core';
2225
import { Agent } from './agent.js';
2326

@@ -37,6 +40,7 @@ export function createCLI(): Command {
3740
.requiredOption('-s, --spec <path>', 'Path or URL to OpenAPI specification')
3841
.option('--no-auth', 'Skip authentication even if required by spec')
3942
.option('--api-key <key>', 'Provide API key or access token directly')
43+
.option('--code-mode', 'Use code mode (2 tools: search + execute)')
4044
.action(async (options) => {
4145
await startAgent(options);
4246
});
@@ -48,6 +52,7 @@ interface StartOptions {
4852
spec: string;
4953
auth: boolean;
5054
apiKey?: string;
55+
codeMode?: boolean;
5156
}
5257

5358
async function startAgent(options: StartOptions): Promise<void> {
@@ -100,6 +105,14 @@ async function startAgent(options: StartOptions): Promise<void> {
100105
// Create executable tools
101106
const tools = createExecutableTools(toolDefs, baseUrl, authManager);
102107

108+
// Convert to AI SDK tools
109+
let aiTools = toAISDKTools(tools);
110+
111+
if (options.codeMode) {
112+
aiTools = toCodeModeTools(aiTools);
113+
console.log(chalk.green('Code mode enabled (2 tools: search + execute)'));
114+
}
115+
103116
// Initialize session
104117
const session: Session = {
105118
baseUrl,
@@ -109,7 +122,7 @@ async function startAgent(options: StartOptions): Promise<void> {
109122
};
110123

111124
// Start chat loop
112-
await startChatLoop(session);
125+
await startChatLoop(session, aiTools, options.codeMode ?? false);
113126
} catch (error) {
114127
spinner.fail();
115128

@@ -136,9 +149,9 @@ async function startAgent(options: StartOptions): Promise<void> {
136149
}
137150
}
138151

139-
async function startChatLoop(session: Session): Promise<void> {
152+
async function startChatLoop(session: Session, aiTools: ToolSet, codeMode: boolean): Promise<void> {
140153
// Initialize agent
141-
const agent = new Agent({ tools: session.tools });
154+
const agent = new Agent({ tools: aiTools });
142155

143156
console.log(chalk.bold('\n--- Spec2Tools ---'));
144157
console.log(chalk.dim('Type your message or use special commands:'));
@@ -167,7 +180,7 @@ async function startChatLoop(session: Session): Promise<void> {
167180
rl.pause();
168181

169182
try {
170-
await handleInput(trimmedInput, session, agent);
183+
await handleInput(trimmedInput, session, agent, codeMode, aiTools);
171184
} catch (error) {
172185
console.error(
173186
chalk.red(
@@ -192,11 +205,13 @@ async function startChatLoop(session: Session): Promise<void> {
192205
async function handleInput(
193206
input: string,
194207
session: Session,
195-
agent: Agent
208+
agent: Agent,
209+
codeMode: boolean,
210+
aiTools: ToolSet
196211
): Promise<void> {
197212
// Handle special commands
198213
if (input.startsWith('/')) {
199-
await handleCommand(input, session, agent);
214+
await handleCommand(input, session, agent, codeMode, aiTools);
200215
return;
201216
}
202217

@@ -216,15 +231,21 @@ async function handleInput(
216231
async function handleCommand(
217232
input: string,
218233
session: Session,
219-
agent: Agent
234+
agent: Agent,
235+
codeMode: boolean,
236+
aiTools: ToolSet
220237
): Promise<void> {
221238
const parts = input.slice(1).split(/\s+/);
222239
const command = parts[0].toLowerCase();
223240
const args = parts.slice(1);
224241

225242
switch (command) {
226243
case 'tools':
227-
listTools(session.tools);
244+
if (codeMode) {
245+
listCodeModeTools(aiTools);
246+
} else {
247+
listTools(session.tools);
248+
}
228249
break;
229250

230251
case 'call':
@@ -267,6 +288,18 @@ function listTools(tools: Tool[]): void {
267288
console.log('');
268289
}
269290

291+
function listCodeModeTools(aiTools: ToolSet): void {
292+
console.log(chalk.bold('\nCode mode tools:'));
293+
294+
Object.entries(aiTools).forEach(([name, t], index) => {
295+
const desc = 'description' in t ? (t.description as string) || '' : '';
296+
console.log(chalk.cyan(`${index + 1}. ${name}`));
297+
console.log(chalk.dim(` ${desc}`));
298+
});
299+
300+
console.log('');
301+
}
302+
270303
async function callTool(tools: Tool[], args: string[]): Promise<void> {
271304
if (args.length === 0) {
272305
console.log(chalk.yellow('Usage: /call <toolName> [--param value ...]'));

packages/core/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# @spec2tools/core
22

3+
## 0.2.0
4+
5+
### Minor Changes
6+
7+
- Add code mode MCP option
8+
39
## 0.1.2
410

511
### Patch Changes

0 commit comments

Comments
 (0)