@hashgraphonline/conversational-agent 0.0.3 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +79 -0
  2. package/dist/cjs/agent-factory.d.ts +5 -0
  3. package/dist/cjs/base-agent.d.ts +95 -0
  4. package/dist/cjs/conversational-agent.d.ts +62 -5
  5. package/dist/cjs/index.cjs +1 -1
  6. package/dist/cjs/index.cjs.map +1 -1
  7. package/dist/cjs/index.d.ts +8 -0
  8. package/dist/cjs/langchain-agent.d.ts +22 -0
  9. package/dist/cjs/mcp/MCPClientManager.d.ts +40 -0
  10. package/dist/cjs/mcp/adapters/langchain.d.ts +8 -0
  11. package/dist/cjs/mcp/helpers.d.ts +45 -0
  12. package/dist/cjs/mcp/types.d.ts +27 -0
  13. package/dist/cjs/plugins/hbar-transfer/AccountBuilder.d.ts +13 -0
  14. package/dist/cjs/plugins/hbar-transfer/HbarTransferPlugin.d.ts +15 -0
  15. package/dist/cjs/plugins/hbar-transfer/TransferHbarTool.d.ts +61 -0
  16. package/dist/cjs/plugins/hbar-transfer/index.d.ts +1 -0
  17. package/dist/cjs/plugins/hbar-transfer/types.d.ts +10 -0
  18. package/dist/cjs/plugins/index.d.ts +1 -0
  19. package/dist/cjs/providers.d.ts +48 -0
  20. package/dist/esm/index.js +18 -3
  21. package/dist/esm/index.js.map +1 -1
  22. package/dist/esm/index10.js +22 -0
  23. package/dist/esm/index10.js.map +1 -0
  24. package/dist/esm/index11.js +104 -0
  25. package/dist/esm/index11.js.map +1 -0
  26. package/dist/esm/index12.js +36 -0
  27. package/dist/esm/index12.js.map +1 -0
  28. package/dist/esm/index13.js +16 -0
  29. package/dist/esm/index13.js.map +1 -0
  30. package/dist/esm/index14.js +127 -0
  31. package/dist/esm/index14.js.map +1 -0
  32. package/dist/esm/index15.js +132 -0
  33. package/dist/esm/index15.js.map +1 -0
  34. package/dist/esm/index16.js +95 -0
  35. package/dist/esm/index16.js.map +1 -0
  36. package/dist/esm/index5.js +42 -202
  37. package/dist/esm/index5.js.map +1 -1
  38. package/dist/esm/index6.js +295 -13
  39. package/dist/esm/index6.js.map +1 -1
  40. package/dist/esm/index7.js +87 -0
  41. package/dist/esm/index7.js.map +1 -0
  42. package/dist/esm/index8.js +255 -0
  43. package/dist/esm/index8.js.map +1 -0
  44. package/dist/esm/index9.js +18 -0
  45. package/dist/esm/index9.js.map +1 -0
  46. package/dist/types/agent-factory.d.ts +5 -0
  47. package/dist/types/base-agent.d.ts +95 -0
  48. package/dist/types/conversational-agent.d.ts +62 -5
  49. package/dist/types/index.d.ts +8 -0
  50. package/dist/types/langchain-agent.d.ts +22 -0
  51. package/dist/types/mcp/MCPClientManager.d.ts +40 -0
  52. package/dist/types/mcp/adapters/langchain.d.ts +8 -0
  53. package/dist/types/mcp/helpers.d.ts +45 -0
  54. package/dist/types/mcp/types.d.ts +27 -0
  55. package/dist/types/plugins/hbar-transfer/AccountBuilder.d.ts +13 -0
  56. package/dist/types/plugins/hbar-transfer/HbarTransferPlugin.d.ts +15 -0
  57. package/dist/types/plugins/hbar-transfer/TransferHbarTool.d.ts +61 -0
  58. package/dist/types/plugins/hbar-transfer/index.d.ts +1 -0
  59. package/dist/types/plugins/hbar-transfer/types.d.ts +10 -0
  60. package/dist/types/plugins/index.d.ts +1 -0
  61. package/dist/types/providers.d.ts +48 -0
  62. package/package.json +12 -6
  63. package/src/agent-factory.ts +21 -0
  64. package/src/base-agent.ts +222 -0
  65. package/src/conversational-agent.ts +204 -102
  66. package/src/index.ts +24 -0
  67. package/src/langchain-agent.ts +333 -0
  68. package/src/mcp/MCPClientManager.ts +148 -0
  69. package/src/mcp/adapters/langchain.ts +185 -0
  70. package/src/mcp/helpers.ts +122 -0
  71. package/src/mcp/types.ts +29 -0
  72. package/src/plugins/hbar-transfer/AccountBuilder.ts +154 -0
  73. package/src/plugins/hbar-transfer/HbarTransferPlugin.ts +66 -0
  74. package/src/plugins/hbar-transfer/TransferHbarTool.ts +49 -0
  75. package/src/plugins/hbar-transfer/index.ts +1 -0
  76. package/src/plugins/hbar-transfer/types.ts +11 -0
  77. package/src/plugins/index.ts +2 -1
  78. package/src/providers.ts +82 -0
@@ -0,0 +1,333 @@
1
+ import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
2
+ import type { StructuredTool } from '@langchain/core/tools';
3
+ import { createOpenAIToolsAgent, AgentExecutor } from 'langchain/agents';
4
+ import {
5
+ ChatPromptTemplate,
6
+ MessagesPlaceholder,
7
+ } from '@langchain/core/prompts';
8
+ import { ChatOpenAI } from '@langchain/openai';
9
+ import {
10
+ calculateTokenCostSync,
11
+ getAllHederaCorePlugins,
12
+ HederaAgentKit,
13
+ TokenUsageCallbackHandler,
14
+ } from 'hedera-agent-kit';
15
+ import type { TokenUsage, CostCalculation } from 'hedera-agent-kit';
16
+ import {
17
+ BaseAgent,
18
+ type ConversationContext,
19
+ type ChatResponse,
20
+ type OperationalMode,
21
+ type UsageStats,
22
+ } from './base-agent';
23
+ import { MCPClientManager } from './mcp/MCPClientManager';
24
+ import { convertMCPToolToLangChain } from './mcp/adapters/langchain';
25
+
26
+ export class LangChainAgent extends BaseAgent {
27
+ private executor: AgentExecutor | undefined;
28
+ private systemMessage = '';
29
+ private mcpManager?: MCPClientManager;
30
+
31
+ async boot(): Promise<void> {
32
+ if (this.initialized) {
33
+ this.logger.warn('Agent already initialized');
34
+ return;
35
+ }
36
+
37
+ try {
38
+ this.agentKit = await this.createAgentKit();
39
+ await this.agentKit.initialize();
40
+
41
+ const modelName =
42
+ this.config.ai?.modelName ||
43
+ process.env.OPENAI_MODEL_NAME ||
44
+ 'gpt-4o-mini';
45
+ this.tokenTracker = new TokenUsageCallbackHandler(modelName);
46
+
47
+ const allTools = this.agentKit.getAggregatedLangChainTools();
48
+ this.tools = this.filterTools(allTools);
49
+
50
+ if (this.config.mcp?.servers && this.config.mcp.servers.length > 0) {
51
+ await this.initializeMCP();
52
+ }
53
+
54
+ this.systemMessage = this.buildSystemPrompt();
55
+
56
+ await this.createExecutor();
57
+
58
+ this.initialized = true;
59
+ this.logger.info('LangChain Hedera agent initialized');
60
+ } catch (error) {
61
+ this.logger.error('Failed to initialize agent:', error);
62
+ throw error;
63
+ }
64
+ }
65
+
66
+ async chat(
67
+ message: string,
68
+ context?: ConversationContext
69
+ ): Promise<ChatResponse> {
70
+ if (!this.initialized || !this.executor) {
71
+ throw new Error('Agent not initialized. Call boot() first.');
72
+ }
73
+
74
+ try {
75
+ const result = await this.executor.invoke({
76
+ input: message,
77
+ chat_history: context?.messages || [],
78
+ });
79
+
80
+ let response: ChatResponse = {
81
+ output: result.output || '',
82
+ message: result.output || '',
83
+ notes: [],
84
+ };
85
+
86
+ const parsedSteps = result?.intermediateSteps?.[0]?.observation;
87
+ if (
88
+ parsedSteps &&
89
+ typeof parsedSteps === 'string' &&
90
+ this.isJSON(parsedSteps)
91
+ ) {
92
+ try {
93
+ const parsed = JSON.parse(parsedSteps);
94
+ response = { ...response, ...parsed };
95
+ } catch (error) {
96
+ this.logger.error('Error parsing intermediate steps:', error);
97
+ }
98
+ }
99
+
100
+ if (!response.output || response.output.trim() === '') {
101
+ response.output = 'Agent action complete.';
102
+ }
103
+
104
+ if (this.tokenTracker) {
105
+ const tokenUsage = this.tokenTracker.getLatestTokenUsage();
106
+ if (tokenUsage) {
107
+ response.tokenUsage = tokenUsage;
108
+ response.cost = calculateTokenCostSync(tokenUsage);
109
+ }
110
+ }
111
+
112
+ return response;
113
+ } catch (error) {
114
+ return this.handleError(error);
115
+ }
116
+ }
117
+
118
+ async shutdown(): Promise<void> {
119
+ if (this.mcpManager) {
120
+ await this.mcpManager.disconnectAll();
121
+ }
122
+
123
+ this.executor = undefined;
124
+ this.agentKit = undefined;
125
+ this.tools = [];
126
+ this.initialized = false;
127
+ this.logger.info('Agent cleaned up');
128
+ }
129
+
130
+ switchMode(mode: OperationalMode): void {
131
+ if (this.config.execution) {
132
+ this.config.execution.operationalMode = mode;
133
+ } else {
134
+ this.config.execution = { operationalMode: mode };
135
+ }
136
+
137
+ if (this.agentKit) {
138
+ this.agentKit.operationalMode = mode;
139
+ }
140
+
141
+ this.systemMessage = this.buildSystemPrompt();
142
+ this.logger.info(`Operational mode switched to: ${mode}`);
143
+ }
144
+
145
+ getUsageStats(): UsageStats {
146
+ if (!this.tokenTracker) {
147
+ return {
148
+ promptTokens: 0,
149
+ completionTokens: 0,
150
+ totalTokens: 0,
151
+ cost: { totalCost: 0 } as CostCalculation,
152
+ };
153
+ }
154
+
155
+ const usage = this.tokenTracker.getTotalTokenUsage();
156
+ const cost = calculateTokenCostSync(usage);
157
+ return { ...usage, cost };
158
+ }
159
+
160
+ getUsageLog(): UsageStats[] {
161
+ if (!this.tokenTracker) {
162
+ return [];
163
+ }
164
+
165
+ return this.tokenTracker.getTokenUsageHistory().map((usage) => ({
166
+ ...usage,
167
+ cost: calculateTokenCostSync(usage),
168
+ }));
169
+ }
170
+
171
+ clearUsageStats(): void {
172
+ if (this.tokenTracker) {
173
+ this.tokenTracker.reset();
174
+ this.logger.info('Usage statistics cleared');
175
+ }
176
+ }
177
+
178
+ private async createAgentKit(): Promise<HederaAgentKit> {
179
+ const corePlugins = getAllHederaCorePlugins();
180
+ const extensionPlugins = this.config.extensions?.plugins || [];
181
+ const plugins = [...corePlugins, ...extensionPlugins];
182
+
183
+ const operationalMode =
184
+ this.config.execution?.operationalMode || 'returnBytes';
185
+ const modelName = this.config.ai?.modelName || 'gpt-4o';
186
+
187
+ return new HederaAgentKit(
188
+ this.config.signer,
189
+ { plugins },
190
+ operationalMode,
191
+ this.config.execution?.userAccountId,
192
+ this.config.execution?.scheduleUserTransactionsInBytesMode ?? true,
193
+ undefined,
194
+ modelName,
195
+ this.config.extensions?.mirrorConfig,
196
+ this.config.debug?.silent ?? false
197
+ );
198
+ }
199
+
200
+ private async createExecutor(): Promise<void> {
201
+ let llm: BaseChatModel;
202
+ if (this.config.ai?.provider && this.config.ai.provider.getModel) {
203
+ llm = this.config.ai.provider.getModel() as BaseChatModel;
204
+ } else if (this.config.ai?.llm) {
205
+ llm = this.config.ai.llm as BaseChatModel;
206
+ } else {
207
+ const apiKey = this.config.ai?.apiKey || process.env.OPENAI_API_KEY;
208
+ if (!apiKey) {
209
+ throw new Error('OpenAI API key required');
210
+ }
211
+
212
+ llm = new ChatOpenAI({
213
+ apiKey,
214
+ modelName: this.config.ai?.modelName || 'gpt-4o-mini',
215
+ temperature: this.config.ai?.temperature ?? 0.1,
216
+ callbacks: this.tokenTracker ? [this.tokenTracker] : [],
217
+ });
218
+ }
219
+
220
+ const prompt = ChatPromptTemplate.fromMessages([
221
+ ['system', this.systemMessage],
222
+ new MessagesPlaceholder('chat_history'),
223
+ ['human', '{input}'],
224
+ new MessagesPlaceholder('agent_scratchpad'),
225
+ ]);
226
+
227
+ const langchainTools = this.tools as unknown as StructuredTool[];
228
+
229
+ const agent = await createOpenAIToolsAgent({
230
+ llm,
231
+ tools: langchainTools,
232
+ prompt,
233
+ });
234
+
235
+ this.executor = new AgentExecutor({
236
+ agent,
237
+ tools: langchainTools,
238
+ verbose: this.config.debug?.verbose ?? false,
239
+ returnIntermediateSteps: true,
240
+ });
241
+ }
242
+
243
+ private handleError(error: unknown): ChatResponse {
244
+ const errorMessage =
245
+ error instanceof Error ? error.message : 'Unknown error';
246
+ this.logger.error('Chat error:', error);
247
+
248
+ let tokenUsage: TokenUsage | undefined;
249
+ let cost: CostCalculation | undefined;
250
+
251
+ if (this.tokenTracker) {
252
+ tokenUsage = this.tokenTracker.getLatestTokenUsage();
253
+ if (tokenUsage) {
254
+ cost = calculateTokenCostSync(tokenUsage);
255
+ }
256
+ }
257
+
258
+ const errorResponse: ChatResponse = {
259
+ output: 'Sorry, I encountered an error processing your request.',
260
+ message: 'Error processing request.',
261
+ error: errorMessage,
262
+ notes: [],
263
+ };
264
+
265
+ if (tokenUsage) {
266
+ errorResponse.tokenUsage = tokenUsage;
267
+ }
268
+
269
+ if (cost) {
270
+ errorResponse.cost = cost;
271
+ }
272
+
273
+ return errorResponse;
274
+ }
275
+
276
+ private async initializeMCP(): Promise<void> {
277
+ this.mcpManager = new MCPClientManager(this.logger);
278
+
279
+ for (const serverConfig of this.config.mcp!.servers!) {
280
+ if (serverConfig.autoConnect === false) {
281
+ this.logger.info(
282
+ `Skipping MCP server ${serverConfig.name} (autoConnect=false)`
283
+ );
284
+ continue;
285
+ }
286
+
287
+ const status = await this.mcpManager.connectServer(serverConfig);
288
+
289
+ if (status.connected) {
290
+ this.logger.info(
291
+ `Connected to MCP server ${status.serverName} with ${status.tools.length} tools`
292
+ );
293
+
294
+ for (const mcpTool of status.tools) {
295
+ const langchainTool = convertMCPToolToLangChain(
296
+ mcpTool,
297
+ this.mcpManager,
298
+ serverConfig
299
+ );
300
+ this.tools.push(langchainTool);
301
+ }
302
+ } else {
303
+ this.logger.error(
304
+ `Failed to connect to MCP server ${status.serverName}: ${status.error}`
305
+ );
306
+ }
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Check if a string is valid JSON
312
+ */
313
+ private isJSON(str: string): boolean {
314
+ if (typeof str !== 'string') return false;
315
+
316
+ const trimmed = str.trim();
317
+ if (!trimmed) return false;
318
+
319
+ if (
320
+ !(trimmed.startsWith('{') && trimmed.endsWith('}')) &&
321
+ !(trimmed.startsWith('[') && trimmed.endsWith(']'))
322
+ ) {
323
+ return false;
324
+ }
325
+
326
+ try {
327
+ JSON.parse(trimmed);
328
+ return true;
329
+ } catch {
330
+ return false;
331
+ }
332
+ }
333
+ }
@@ -0,0 +1,148 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
3
+ import type { MCPServerConfig, MCPToolInfo, MCPConnectionStatus } from './types';
4
+ import { Logger } from '@hashgraphonline/standards-sdk';
5
+
6
+ /**
7
+ * Manages connections to MCP servers and tool discovery
8
+ */
9
+ export class MCPClientManager {
10
+ private clients: Map<string, Client> = new Map();
11
+ private tools: Map<string, MCPToolInfo[]> = new Map();
12
+ private logger: Logger;
13
+
14
+ constructor(logger: Logger) {
15
+ this.logger = logger;
16
+ }
17
+
18
+ /**
19
+ * Connect to an MCP server and discover its tools
20
+ */
21
+ async connectServer(config: MCPServerConfig): Promise<MCPConnectionStatus> {
22
+ try {
23
+ if (this.isServerConnected(config.name)) {
24
+ return {
25
+ serverName: config.name,
26
+ connected: false,
27
+ error: `Server ${config.name} is already connected`,
28
+ tools: [],
29
+ };
30
+ }
31
+
32
+ if (config.transport && config.transport !== 'stdio') {
33
+ throw new Error(`Transport ${config.transport} not yet supported`);
34
+ }
35
+
36
+ const transport = new StdioClientTransport({
37
+ command: config.command,
38
+ args: config.args,
39
+ ...(config.env && { env: config.env }),
40
+ });
41
+
42
+ const client = new Client({
43
+ name: `conversational-agent-${config.name}`,
44
+ version: '1.0.0',
45
+ }, {
46
+ capabilities: {},
47
+ });
48
+
49
+ await client.connect(transport);
50
+ this.clients.set(config.name, client);
51
+
52
+ const toolsResponse = await client.listTools();
53
+ const toolsWithServer: MCPToolInfo[] = toolsResponse.tools.map(tool => ({
54
+ ...tool,
55
+ serverName: config.name,
56
+ }));
57
+
58
+ this.tools.set(config.name, toolsWithServer);
59
+ this.logger.info(`Connected to MCP server ${config.name} with ${toolsWithServer.length} tools`);
60
+
61
+ return {
62
+ serverName: config.name,
63
+ connected: true,
64
+ tools: toolsWithServer,
65
+ };
66
+ } catch (error) {
67
+ this.logger.error(`Failed to connect to MCP server ${config.name}:`, error);
68
+ return {
69
+ serverName: config.name,
70
+ connected: false,
71
+ error: error instanceof Error ? error.message : 'Unknown error',
72
+ tools: [],
73
+ };
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Execute a tool on a specific MCP server
79
+ */
80
+ async executeTool(serverName: string, toolName: string, args: Record<string, unknown>): Promise<unknown> {
81
+ const client = this.clients.get(serverName);
82
+ if (!client) {
83
+ throw new Error(`MCP server ${serverName} not connected`);
84
+ }
85
+
86
+ this.logger.debug(`Executing MCP tool ${toolName} on server ${serverName}`, args);
87
+
88
+ try {
89
+ const result = await client.callTool({
90
+ name: toolName,
91
+ arguments: args,
92
+ });
93
+
94
+ return result;
95
+ } catch (error) {
96
+ this.logger.error(`Error executing MCP tool ${toolName}:`, error);
97
+ throw error;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Disconnect all MCP servers
103
+ */
104
+ async disconnectAll(): Promise<void> {
105
+ for (const [name, client] of this.clients) {
106
+ try {
107
+ await client.close();
108
+ this.logger.info(`Disconnected from MCP server ${name}`);
109
+ } catch (error) {
110
+ this.logger.error(`Error disconnecting MCP server ${name}:`, error);
111
+ }
112
+ }
113
+ this.clients.clear();
114
+ this.tools.clear();
115
+ }
116
+
117
+ /**
118
+ * Get all discovered tools from all connected servers
119
+ */
120
+ getAllTools(): MCPToolInfo[] {
121
+ const allTools: MCPToolInfo[] = [];
122
+ for (const tools of this.tools.values()) {
123
+ allTools.push(...tools);
124
+ }
125
+ return allTools;
126
+ }
127
+
128
+ /**
129
+ * Get tools from a specific server
130
+ */
131
+ getServerTools(serverName: string): MCPToolInfo[] {
132
+ return this.tools.get(serverName) || [];
133
+ }
134
+
135
+ /**
136
+ * Check if a server is connected
137
+ */
138
+ isServerConnected(serverName: string): boolean {
139
+ return this.clients.has(serverName);
140
+ }
141
+
142
+ /**
143
+ * Get list of connected server names
144
+ */
145
+ getConnectedServers(): string[] {
146
+ return Array.from(this.clients.keys());
147
+ }
148
+ }
@@ -0,0 +1,185 @@
1
+ import { DynamicStructuredTool } from '@langchain/core/tools';
2
+ import { z } from 'zod';
3
+ import type { MCPToolInfo, MCPServerConfig } from '../types';
4
+ import type { MCPClientManager } from '../MCPClientManager';
5
+
6
+ /**
7
+ * Convert an MCP tool to a LangChain DynamicStructuredTool
8
+ */
9
+ export function convertMCPToolToLangChain(
10
+ tool: MCPToolInfo,
11
+ mcpManager: MCPClientManager,
12
+ serverConfig?: MCPServerConfig
13
+ ): DynamicStructuredTool {
14
+ const zodSchema = jsonSchemaToZod(tool.inputSchema);
15
+
16
+ const sanitizedName = `${tool.serverName}_${tool.name}`.replace(
17
+ /[^a-zA-Z0-9_]/g,
18
+ '_'
19
+ );
20
+
21
+ let description = tool.description || `MCP tool ${tool.name} from ${tool.serverName}`;
22
+
23
+ if (serverConfig?.toolDescriptions?.[tool.name]) {
24
+ description = `${description}\n\n${serverConfig.toolDescriptions[tool.name]}`;
25
+ }
26
+
27
+ if (serverConfig?.additionalContext) {
28
+ description = `${description}\n\nContext: ${serverConfig.additionalContext}`;
29
+ }
30
+
31
+ return new DynamicStructuredTool({
32
+ name: sanitizedName,
33
+ description,
34
+ schema: zodSchema,
35
+ func: async (input) => {
36
+ try {
37
+ const result = await mcpManager.executeTool(
38
+ tool.serverName,
39
+ tool.name,
40
+ input
41
+ );
42
+
43
+ if (typeof result === 'string') {
44
+ return result;
45
+ } else if (
46
+ result &&
47
+ typeof result === 'object' &&
48
+ 'content' in result
49
+ ) {
50
+ const content = (result as { content: unknown }).content;
51
+ if (Array.isArray(content)) {
52
+ const textParts = content
53
+ .filter(
54
+ (item): item is { type: string; text: string } =>
55
+ typeof item === 'object' &&
56
+ item !== null &&
57
+ 'type' in item &&
58
+ item.type === 'text' &&
59
+ 'text' in item
60
+ )
61
+ .map((item) => item.text);
62
+ return textParts.join('\n');
63
+ }
64
+ return JSON.stringify(content);
65
+ }
66
+
67
+ return JSON.stringify(result);
68
+ } catch (error) {
69
+ const errorMessage =
70
+ error instanceof Error ? error.message : 'Unknown error';
71
+ return `Error executing MCP tool ${tool.name}: ${errorMessage}`;
72
+ }
73
+ },
74
+ });
75
+ }
76
+
77
+ /**
78
+ * Convert JSON Schema to Zod schema
79
+ * This is a simplified converter that handles common cases
80
+ */
81
+ function jsonSchemaToZod(schema: unknown): z.ZodTypeAny {
82
+ if (!schema || typeof schema !== 'object') {
83
+ return z.object({});
84
+ }
85
+
86
+ const schemaObj = schema as Record<string, unknown>;
87
+
88
+ if (schemaObj.type && schemaObj.type !== 'object') {
89
+ return convertType(schemaObj);
90
+ }
91
+
92
+ if (!schemaObj.properties || typeof schemaObj.properties !== 'object') {
93
+ return z.object({});
94
+ }
95
+
96
+ const shape: Record<string, z.ZodTypeAny> = {};
97
+
98
+ for (const [key, value] of Object.entries(schemaObj.properties)) {
99
+ let zodType = convertType(value);
100
+
101
+ const isRequired =
102
+ Array.isArray(schemaObj.required) && schemaObj.required.includes(key);
103
+ if (!isRequired) {
104
+ zodType = zodType.optional();
105
+ }
106
+
107
+ shape[key] = zodType;
108
+ }
109
+
110
+ return z.object(shape);
111
+ }
112
+
113
+ /**
114
+ * Convert a single JSON Schema type to Zod
115
+ */
116
+ function convertType(schema: unknown): z.ZodTypeAny {
117
+ if (!schema || typeof schema !== 'object' || !('type' in schema)) {
118
+ return z.unknown();
119
+ }
120
+
121
+ const schemaObj = schema as {
122
+ type: string;
123
+ enum?: unknown[];
124
+ items?: unknown;
125
+ };
126
+ let zodType: z.ZodTypeAny;
127
+
128
+ switch (schemaObj.type) {
129
+ case 'string':
130
+ zodType = z.string();
131
+ if (schemaObj.enum && Array.isArray(schemaObj.enum)) {
132
+ zodType = z.enum(schemaObj.enum as [string, ...string[]]);
133
+ }
134
+ break;
135
+
136
+ case 'number':
137
+ zodType = z.number();
138
+ if ('minimum' in schemaObj && typeof schemaObj.minimum === 'number') {
139
+ zodType = (zodType as z.ZodNumber).min(schemaObj.minimum);
140
+ }
141
+ if ('maximum' in schemaObj && typeof schemaObj.maximum === 'number') {
142
+ zodType = (zodType as z.ZodNumber).max(schemaObj.maximum);
143
+ }
144
+ break;
145
+
146
+ case 'integer':
147
+ zodType = z.number().int();
148
+ if ('minimum' in schemaObj && typeof schemaObj.minimum === 'number') {
149
+ zodType = (zodType as z.ZodNumber).min(schemaObj.minimum);
150
+ }
151
+ if ('maximum' in schemaObj && typeof schemaObj.maximum === 'number') {
152
+ zodType = (zodType as z.ZodNumber).max(schemaObj.maximum);
153
+ }
154
+ break;
155
+
156
+ case 'boolean':
157
+ zodType = z.boolean();
158
+ break;
159
+
160
+ case 'array':
161
+ if (schemaObj.items) {
162
+ zodType = z.array(convertType(schemaObj.items));
163
+ } else {
164
+ zodType = z.array(z.unknown());
165
+ }
166
+ break;
167
+
168
+ case 'object':
169
+ if ('properties' in schemaObj) {
170
+ zodType = jsonSchemaToZod(schemaObj);
171
+ } else {
172
+ zodType = z.object({}).passthrough();
173
+ }
174
+ break;
175
+
176
+ default:
177
+ zodType = z.unknown();
178
+ }
179
+
180
+ if ('description' in schemaObj && typeof schemaObj.description === 'string') {
181
+ zodType = zodType.describe(schemaObj.description);
182
+ }
183
+
184
+ return zodType;
185
+ }