Refactor/clis (#6)

This commit is contained in:
gzuuus
2025-03-19 17:31:33 +01:00
committed by GitHub
parent a75fb9de1c
commit 09debf6807
28 changed files with 219 additions and 104 deletions

2
.gitignore vendored
View File

@@ -178,4 +178,4 @@ dist
codebase* codebase*
.aider* .aider*
.prettierrc .prettierrc
config.yml config.dvmcp.yml

View File

@@ -42,8 +42,8 @@ This specification defines these event kinds:
| Kind | Description | | Kind | Description |
| ----- | ------------------------------------- | | ----- | ------------------------------------- |
| 31990 | DVM Service Announcement (via NIP-89) | | 31990 | DVM Service Announcement (via NIP-89) |
| 5910 | DVM-MCP Bridge Requests | | 5910 | DVMCP Bridge Requests |
| 6910 | DVM-MCP Bridge Responses | | 6910 | DVMCP Bridge Responses |
| 7000 | Job Feedback | | 7000 | Job Feedback |
Operations are differentiated using the `c` tag, which specifies the command being executed: Operations are differentiated using the `c` tag, which specifies the command being executed:
@@ -339,7 +339,7 @@ For any error, DVMs MUST:
sequenceDiagram sequenceDiagram
participant Client as Nostr Client participant Client as Nostr Client
participant Relay as Nostr Relay participant Relay as Nostr Relay
participant DVM as MCP-DVM Bridge participant DVM as DVMCP-Bridge
participant Server as MCP Server participant Server as MCP Server
rect rgb(240, 240, 240) rect rgb(240, 240, 240)

View File

@@ -4,4 +4,4 @@ node_modules
.DS_Store .DS_Store
*.test.ts *.test.ts
*.test.js *.test.js
config.yml config.dvmcp.yml

View File

@@ -12,11 +12,11 @@ A bridge implementation that connects Model Context Protocol (MCP) servers to No
## Configuration ## Configuration
When the package is run for the first time, it will detect if the `config.yml` file exists, and if not, it will launch a configuration wizard to help you create the configuration file. You can also create your configuration file by copying `config.example.yml` and changing the values of the fields When the package is run for the first time, it will detect if the `'config.dvmcp.yml'` file exists, and if not, it will launch a configuration wizard to help you create the configuration file. You can also create your configuration file by copying `config.example.yml` and changing the values of the fields
```bash ```bash
cp config.example.yml config.yml cp config.example.yml config.dvmcp.yml
nano config.yml nano config.dvmcp.yml
``` ```
## Usage ## Usage

View File

@@ -14,7 +14,7 @@ import {
import { argv } from 'process'; import { argv } from 'process';
import type { Config } from './src/types'; import type { Config } from './src/types';
const configPath = join(process.cwd(), 'config.yml'); const configPath = join(process.cwd(), 'config.dvmcp.yml');
const configFields: Record<string, FieldConfig> = { const configFields: Record<string, FieldConfig> = {
nostr: { nostr: {

View File

@@ -15,7 +15,9 @@ mcp:
# Required client information # Required client information
clientName: "DVM MCP Bridge Client" clientName: "DVM MCP Bridge Client"
clientVersion: "1.0.0" clientVersion: "1.0.0"
# optional metadata
picture: "https://image.nostr.build/5bf2e2eb3b858bf72c23e53ed1f41ed0f65b2c8a805eaa48dd506b7cfec4ab88.png"
website: "https://github.com/gzuuus/dvmcp"
# MCP Servers Configuration accepts multiple servers # MCP Servers Configuration accepts multiple servers
servers: servers:
- name: "server1" - name: "server1"

View File

@@ -0,0 +1,18 @@
nostr:
privateKey: 0e79e5e90754cb23275477a32bd1c42bee28dc78a0b10693dfd8cac6852363fd
relayUrls:
- ws://localhost:10547
mcp:
name: DVM MCP Bridge
about: MCP-enabled DVM providing AI and computational tools
clientName: DVM MCP Bridge Client
clientVersion: 1.0.0
servers:
- name: hello
command: bun
args:
- --cwd
- /home/user/Documents/dev/gz/js-ts/bun/mcp-echo
- start:mcp
whitelist:
allowedPubkeys: []

View File

@@ -1,6 +1,6 @@
{ {
"name": "@dvmcp/bridge", "name": "@dvmcp/bridge",
"version": "0.1.6", "version": "0.1.11",
"description": "Bridge connecting MCP servers to Nostr's DVM ecosystem", "description": "Bridge connecting MCP servers to Nostr's DVM ecosystem",
"module": "index.ts", "module": "index.ts",
"type": "module", "type": "module",
@@ -35,7 +35,7 @@
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"nostr-tools": "^2.10.4", "nostr-tools": "^2.10.4",
"yaml": "^2.7.0", "yaml": "^2.7.0",
"@dvmcp/commons": "^0.1.1" "@dvmcp/commons": "^0.1.2"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"

View File

@@ -37,6 +37,9 @@ export class NostrAnnouncer {
content: JSON.stringify({ content: JSON.stringify({
name: CONFIG.mcp.name, name: CONFIG.mcp.name,
about: CONFIG.mcp.about, about: CONFIG.mcp.about,
picture: CONFIG.mcp.picture,
website: CONFIG.mcp.website,
banner: CONFIG.mcp.banner,
tools: tools, tools: tools,
}), }),
tags: [ tags: [

View File

@@ -4,7 +4,7 @@ import { existsSync, readFileSync } from 'fs';
import { HEX_KEYS_REGEX } from '@dvmcp/commons/constants'; import { HEX_KEYS_REGEX } from '@dvmcp/commons/constants';
import type { Config, MCPServerConfig } from './types'; import type { Config, MCPServerConfig } from './types';
const CONFIG_PATH = join(process.cwd(), 'config.yml'); const CONFIG_PATH = join(process.cwd(), 'config.dvmcp.yml');
const TEST_CONFIG: Config = { const TEST_CONFIG: Config = {
nostr: { nostr: {
@@ -87,7 +87,7 @@ function loadConfig(): Config {
if (!existsSync(CONFIG_PATH)) { if (!existsSync(CONFIG_PATH)) {
throw new Error( throw new Error(
'No config.yml file found. Please create one based on config.example.yml' 'No config.dvmcp.yml file found. Please create one based on config.example.yml'
); );
} }
@@ -117,6 +117,9 @@ function loadConfig(): Config {
rawConfig.mcp?.clientVersion, rawConfig.mcp?.clientVersion,
'mcp.clientVersion' 'mcp.clientVersion'
), ),
picture: rawConfig.mcp?.picture,
website: rawConfig.mcp?.website,
banner: rawConfig.mcp?.banner,
servers: validateMCPServers(rawConfig.mcp?.servers), servers: validateMCPServers(rawConfig.mcp?.servers),
}, },
whitelist: { whitelist: {

View File

@@ -24,7 +24,10 @@ export class DVMBridge {
} }
private isWhitelisted(pubkey: string): boolean { private isWhitelisted(pubkey: string): boolean {
if (!CONFIG.whitelist.allowedPubkeys) { if (
!CONFIG.whitelist.allowedPubkeys ||
CONFIG.whitelist.allowedPubkeys.size == 0
) {
return true; return true;
} }
return CONFIG.whitelist.allowedPubkeys.has(pubkey); return CONFIG.whitelist.allowedPubkeys.has(pubkey);
@@ -47,7 +50,12 @@ export class DVMBridge {
await this.nostrAnnouncer.updateAnnouncement(); await this.nostrAnnouncer.updateAnnouncement();
console.log('Setting up request handlers...'); console.log('Setting up request handlers...');
this.relayHandler.subscribeToRequests(this.handleRequest.bind(this)); const publicKey = keyManager.getPublicKey();
this.relayHandler.subscribeToRequests(this.handleRequest.bind(this), {
kinds: [TOOL_REQUEST_KIND],
'#p': [publicKey],
since: Math.floor(Date.now() / 1000),
});
this.isRunning = true; this.isRunning = true;
console.log('DVM Bridge is now running and ready to handle requests'); console.log('DVM Bridge is now running and ready to handle requests');
@@ -88,14 +96,14 @@ export class DVMBridge {
tools, tools,
}), }),
tags: [ tags: [
['request', JSON.stringify(event)], ['c', 'list-tools-response'],
['e', event.id], ['e', event.id],
['p', event.pubkey], ['p', event.pubkey],
], ],
}); });
await this.relayHandler.publishEvent(response); await this.relayHandler.publishEvent(response);
} else { } else if (command === 'execute-tool') {
const jobRequest = JSON.parse(event.content); const jobRequest = JSON.parse(event.content);
const processingStatus = keyManager.signEvent({ const processingStatus = keyManager.signEvent({
...keyManager.createEventTemplate(DVM_NOTICE_KIND), ...keyManager.createEventTemplate(DVM_NOTICE_KIND),
@@ -125,7 +133,7 @@ export class DVMBridge {
...keyManager.createEventTemplate(TOOL_RESPONSE_KIND), ...keyManager.createEventTemplate(TOOL_RESPONSE_KIND),
content: JSON.stringify(result), content: JSON.stringify(result),
tags: [ tags: [
['request', JSON.stringify(event)], ['c', 'execute-tool-response'],
['e', event.id], ['e', event.id],
['p', event.pubkey], ['p', event.pubkey],
], ],
@@ -159,6 +167,7 @@ export class DVMBridge {
], ],
}); });
await this.relayHandler.publishEvent(errorStatus); await this.relayHandler.publishEvent(errorStatus);
return;
} }
} catch (error) { } catch (error) {
console.error('Error handling request:', error); console.error('Error handling request:', error);

View File

@@ -22,4 +22,9 @@ describe('KeyManager', () => {
expect(signedEvent.sig).toBeDefined(); expect(signedEvent.sig).toBeDefined();
expect(signedEvent.pubkey).toBe(keyManager.pubkey); expect(signedEvent.pubkey).toBe(keyManager.pubkey);
}); });
test('should get public key', () => {
const publicKey = keyManager.getPublicKey();
expect(publicKey).toBeDefined();
});
}); });

View File

@@ -9,7 +9,10 @@ describe('MCPPool', () => {
let transports: any[] = []; let transports: any[] = [];
const serverNames = ['server1', 'server2', 'server3', 'server4']; const serverNames = ['server1', 'server2', 'server3', 'server4'];
beforeAll(async () => { beforeAll(async () => {
const mockServerPath = join(import.meta.dir, 'mock-server.ts'); const mockServerPath = join(
import.meta.dir,
'../../dvmcp-commons/mock-server.ts'
);
const serverConfigs = serverNames.map((name) => ({ const serverConfigs = serverNames.map((name) => ({
name, name,

View File

@@ -1,30 +0,0 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
export const createMockServer = async (name: string) => {
const server = new McpServer({
name: `Mock ${name}`,
version: '1.0.0',
});
server.tool(
`${name}-echo`,
`Echo tool for ${name}`,
{
text: z.string(),
},
async ({ text }) => ({
content: [{ type: 'text' as const, text: `[${name}] ${text}` }],
})
);
const transport = new StdioServerTransport();
await server.connect(transport);
return { server, transport };
};
if (import.meta.path === Bun.main) {
await createMockServer(process.argv[2] || 'default');
}

View File

@@ -8,6 +8,9 @@ export interface MCPConfig {
about: string; about: string;
clientName: string; clientName: string;
clientVersion: string; clientVersion: string;
picture?: string;
website?: string;
banner?: string;
servers: MCPServerConfig[]; servers: MCPServerConfig[];
} }

View File

@@ -1,6 +1,6 @@
import { createInterface } from 'node:readline'; import { createInterface } from 'node:readline';
import { stringify } from 'yaml'; import { parse, stringify } from 'yaml';
import { writeFileSync, existsSync } from 'node:fs'; import { writeFileSync, existsSync, readFileSync } from 'node:fs';
import { randomBytes } from 'node:crypto'; import { randomBytes } from 'node:crypto';
import { HEX_KEYS_REGEX } from './constants'; import { HEX_KEYS_REGEX } from './constants';
@@ -47,10 +47,21 @@ export interface FieldConfig {
} }
export class ConfigGenerator<T extends Record<string, any>> { export class ConfigGenerator<T extends Record<string, any>> {
private currentConfig: T | null = null;
constructor( constructor(
private configPath: string, private configPath: string,
private fields: Record<string, FieldConfig> private fields: Record<string, FieldConfig>
) {} ) {
if (existsSync(configPath)) {
try {
this.currentConfig = parse(readFileSync(configPath, 'utf8'));
} catch (error) {
console.warn(
`${CONFIG_EMOJIS.INFO} Could not parse existing configuration`
);
}
}
}
private async prompt(question: string, defaultValue = ''): Promise<string> { private async prompt(question: string, defaultValue = ''): Promise<string> {
const rl = createInterface({ const rl = createInterface({
@@ -112,6 +123,27 @@ export class ConfigGenerator<T extends Record<string, any>> {
): Promise<any> { ): Promise<any> {
const emoji = config.emoji || CONFIG_EMOJIS.PROMPT; const emoji = config.emoji || CONFIG_EMOJIS.PROMPT;
if (currentValue !== undefined) {
if (config.type !== 'nested' && config.type !== 'object-array') {
console.log(
`${CONFIG_EMOJIS.INFO} Current value: ${
typeof currentValue === 'object'
? JSON.stringify(currentValue)
: currentValue
}`
);
}
const keepCurrent = await this.promptYesNo(
`${emoji} Keep current ${fieldName}?`,
true
);
if (keepCurrent) {
return currentValue;
}
}
if (config.type === 'nested') { if (config.type === 'nested') {
console.log(`\n${emoji} ${config.description || fieldName}`); console.log(`\n${emoji} ${config.description || fieldName}`);
} }
@@ -151,7 +183,20 @@ export class ConfigGenerator<T extends Record<string, any>> {
array.forEach((item: string, index: number) => { array.forEach((item: string, index: number) => {
console.log(`${CONFIG_EMOJIS.INFO} ${index + 1}. ${item}`); console.log(`${CONFIG_EMOJIS.INFO} ${index + 1}. ${item}`);
}); });
console.log('');
if (await this.promptYesNo(`${emoji} Remove any items?`, false)) {
while (true) {
const index =
parseInt(
await this.prompt('Enter index to remove (0 to finish):')
) - 1;
if (isNaN(index) || index < 0) break;
if (index < array.length) {
array.splice(index, 1);
console.log('Item removed');
}
}
}
} }
if (await this.promptYesNo(`${emoji} Add ${fieldName}?`, true)) { if (await this.promptYesNo(`${emoji} Add ${fieldName}?`, true)) {
@@ -187,7 +232,7 @@ export class ConfigGenerator<T extends Record<string, any>> {
}, },
'object-array': async () => { 'object-array': async () => {
const array: ArrayItem[] = currentValue || []; let array: ArrayItem[] = currentValue || [];
if (array.length > 0) { if (array.length > 0) {
console.log('\nCurrent servers:'); console.log('\nCurrent servers:');
array.forEach((item: ArrayItem, index: number) => { array.forEach((item: ArrayItem, index: number) => {
@@ -196,19 +241,49 @@ export class ConfigGenerator<T extends Record<string, any>> {
); );
}); });
console.log(''); console.log('');
} const keepCurrent = await this.promptYesNo(
`${emoji} Keep current ${fieldName}?`,
true
);
if (!keepCurrent) {
array = [];
console.log(`${CONFIG_EMOJIS.INFO} Cleared existing servers.`);
} else {
if ( if (
(await this.promptYesNo(`${emoji} Add new ${fieldName}?`, true)) && await this.promptYesNo(
config.fields `${emoji} Remove any existing servers?`,
false
)
) { ) {
while (true) { while (true) {
const item = await this.handleObjectArrayItem(config.fields); const index =
parseInt(
await this.prompt(
'Enter server number to remove (0 to finish):'
)
) - 1;
if (isNaN(index) || index < 0) break;
if (index < array.length) {
console.log(
`${CONFIG_EMOJIS.INFO} Removed server: ${array[index].name}`
);
array.splice(index, 1);
}
}
}
}
}
if (await this.promptYesNo(`${emoji} Add new ${fieldName}?`, true)) {
while (true) {
const item = await this.handleObjectArrayItem(config.fields!);
if (!item) break; if (!item) break;
array.push(item as ArrayItem); array.push(item as ArrayItem);
console.log(`${CONFIG_EMOJIS.INFO} Added new server: ${item.name}`);
console.log(''); console.log('');
} }
} }
return array; return array;
}, },
@@ -269,19 +344,15 @@ export class ConfigGenerator<T extends Record<string, any>> {
} }
async generate(): Promise<T> { async generate(): Promise<T> {
console.log(`\n${CONFIG_EMOJIS.SETUP} Configuration Setup\n`);
if (existsSync(this.configPath)) {
const reconfigure = await this.promptYesNo(
`${CONFIG_EMOJIS.PROMPT} Configuration exists. Reconfigure?`,
true
);
if (!reconfigure) process.exit(0);
}
const config: Record<string, any> = {}; const config: Record<string, any> = {};
for (const [fieldName, fieldConfig] of Object.entries(this.fields)) { for (const [fieldName, fieldConfig] of Object.entries(this.fields)) {
config[fieldName] = await this.handleField(fieldName, fieldConfig); const currentValue = this.currentConfig?.[fieldName];
config[fieldName] = await this.handleField(
fieldName,
fieldConfig,
true,
currentValue
);
} }
writeFileSync(this.configPath, stringify(config)); writeFileSync(this.configPath, stringify(config));

View File

@@ -22,6 +22,9 @@ export const createKeyManager = (privateKeyHex: string) => {
content: '', content: '',
}; };
} }
getPublicKey(): string {
return this.pubkey;
}
} }
return new Manager(); return new Manager();

View File

@@ -1,6 +1,6 @@
{ {
"name": "@dvmcp/commons", "name": "@dvmcp/commons",
"version": "0.1.1", "version": "0.1.2",
"description": "Shared utilities for DVMCP packages", "description": "Shared utilities for DVMCP packages",
"type": "module", "type": "module",
"exports": { "exports": {

View File

@@ -4,4 +4,4 @@ node_modules
.DS_Store .DS_Store
*.test.ts *.test.ts
*.test.js *.test.js
config.yml config.dvmcp.yml

View File

@@ -11,11 +11,11 @@ A MCP server implementation that aggregates tools from DVMs across the Nostr net
## Configuration ## Configuration
When the package is run for the first time, it will detect if the `config.yml` file exists, and if not, it will launch a configuration wizard to help you create the configuration file. You can also create your configuration file by copying `config.example.yml` and changing the values of the fields When the package is run for the first time, it will detect if the `config.dvmcp.yml` file exists, and if not, it will launch a configuration wizard to help you create the configuration file. You can also create your configuration file by copying `config.example.yml` and changing the values of the fields
```bash ```bash
cp config.example.yml config.yml cp config.example.yml config.dvmcp.yml
nano config.yml nano config.dvmcp.yml
``` ```
## Usage ## Usage

View File

@@ -12,7 +12,7 @@ import type { Config } from './src/config.js';
import { argv } from 'process'; import { argv } from 'process';
import { existsSync } from 'fs'; import { existsSync } from 'fs';
const configPath = join(process.cwd(), 'config.yml'); const configPath = join(process.cwd(), 'config.dvmcp.yml');
const configFields: Record<string, FieldConfig> = { const configFields: Record<string, FieldConfig> = {
nostr: { nostr: {

View File

@@ -0,0 +1,10 @@
nostr:
privateKey: c5af6a2588250d3dfe82a9cc1c054697d7483b53850d7e6552620ff8e565630c
relayUrls:
- wss://relay.dvmcp.fun
mcp:
name: DVMCP Discovery
version: 1.0.0
about: DVMCP Discovery Server for aggregating MCP tools from DVMs
whitelist:
allowedDVMs: []

View File

@@ -1,6 +1,6 @@
{ {
"name": "@dvmcp/discovery", "name": "@dvmcp/discovery",
"version": "0.1.7", "version": "0.1.11",
"description": "Discovery service for MCP tools in the Nostr DVM ecosystem", "description": "Discovery service for MCP tools in the Nostr DVM ecosystem",
"module": "index.ts", "module": "index.ts",
"type": "module", "type": "module",
@@ -33,7 +33,7 @@
"@modelcontextprotocol/sdk": "^1.5.0", "@modelcontextprotocol/sdk": "^1.5.0",
"nostr-tools": "^2.10.4", "nostr-tools": "^2.10.4",
"yaml": "^2.7.0", "yaml": "^2.7.0",
"@dvmcp/commons": "^0.1.1" "@dvmcp/commons": "^0.1.2"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"

View File

@@ -18,7 +18,7 @@ export interface Config {
}; };
} }
const CONFIG_PATH = join(process.cwd(), 'config.yml'); const CONFIG_PATH = join(process.cwd(), 'config.dvmcp.yml');
const TEST_CONFIG: Config = { const TEST_CONFIG: Config = {
nostr: { nostr: {
@@ -79,7 +79,7 @@ function loadConfig(): Config {
if (!existsSync(CONFIG_PATH)) { if (!existsSync(CONFIG_PATH)) {
throw new Error( throw new Error(
'No config.yml file found. Please create one based on config.example.yml' 'No config.dvmcp.yml file found. Please create one based on config.example.yml'
); );
} }

View File

@@ -61,7 +61,7 @@ export class DiscoveryServer {
if (!announcement?.tools) return; if (!announcement?.tools) return;
for (const tool of announcement.tools) { for (const tool of announcement.tools) {
const toolId = `${event.pubkey.slice(0, 12)}:${tool.name}`; const toolId = `${tool.name}:${event.pubkey.slice(0, 4)}`;
this.toolRegistry.registerTool(toolId, tool); this.toolRegistry.registerTool(toolId, tool);
} }
} catch (error) { } catch (error) {
@@ -72,7 +72,7 @@ export class DiscoveryServer {
private isAllowedDVM(pubkey: string): boolean { private isAllowedDVM(pubkey: string): boolean {
if ( if (
!CONFIG.whitelist?.allowedDVMs || !CONFIG.whitelist?.allowedDVMs ||
CONFIG.whitelist.allowedDVMs.size === 0 CONFIG.whitelist.allowedDVMs.size == 0
) { ) {
return true; return true;
} }

View File

@@ -56,7 +56,7 @@ describe('DiscoveryServer E2E', () => {
const toolIds = Array.from(toolRegistry['discoveredTools'].keys()); const toolIds = Array.from(toolRegistry['discoveredTools'].keys());
console.log('Available tool IDs:', toolIds); console.log('Available tool IDs:', toolIds);
const toolId = toolIds.find((id) => id.endsWith(`:${mockTool.name}`)); const toolId = toolIds.find((id) => id.startsWith(`${mockTool.name}`));
expect(toolId).toBeDefined(); expect(toolId).toBeDefined();
console.log('Selected tool ID:', toolId); console.log('Selected tool ID:', toolId);

View File

@@ -104,10 +104,18 @@ export class ToolExecutor {
private createToolRequest(tool: Tool, params: unknown): Event { private createToolRequest(tool: Tool, params: unknown): Event {
const request = this.keyManager.createEventTemplate(TOOL_REQUEST_KIND); const request = this.keyManager.createEventTemplate(TOOL_REQUEST_KIND);
const parameters =
!tool.inputSchema.properties ||
Object.keys(tool.inputSchema.properties).length === 0
? {}
: params;
request.content = JSON.stringify({ request.content = JSON.stringify({
name: tool.name, name: tool.name,
parameters: params, parameters,
}); });
request.tags.push(['c', 'execute-tool']); request.tags.push(['c', 'execute-tool']);
return this.keyManager.signEvent(request); return this.keyManager.signEvent(request);
} }

View File

@@ -81,27 +81,34 @@ export class ToolRegistry {
} }
private mapJsonSchemaToZod(schema: Tool['inputSchema']): z.ZodRawShape { private mapJsonSchemaToZod(schema: Tool['inputSchema']): z.ZodRawShape {
if (!schema.properties || Object.keys(schema.properties).length === 0) {
return { _: z.object({}).optional() };
}
const properties: z.ZodRawShape = {}; const properties: z.ZodRawShape = {};
if (schema.properties) {
for (const [key, prop] of Object.entries(schema.properties)) { for (const [key, prop] of Object.entries(schema.properties)) {
if (typeof prop === 'object' && prop && 'type' in prop) { if (typeof prop === 'object' && prop && 'type' in prop) {
let zodType: z.ZodType;
switch (prop.type) { switch (prop.type) {
case 'string': case 'string':
properties[key] = z.string(); zodType = z.string();
break; break;
case 'number': case 'number':
properties[key] = z.number(); zodType = z.number();
break; break;
case 'integer': case 'integer':
properties[key] = z.number().int(); zodType = z.number().int();
break; break;
case 'boolean': case 'boolean':
properties[key] = z.boolean(); zodType = z.boolean();
break; break;
default: default:
properties[key] = z.any(); zodType = z.any();
}
} }
properties[key] =
Array.isArray(schema.required) && schema.required.includes(key)
? zodType
: zodType.optional();
} }
} }
return properties; return properties;