Skip to main content
Glama
IBM

IBM i MCP Server

Official
by IBM
yaml-parser.test.ts12.9 kB
/** * @fileoverview Simplified unit tests for YAML parsing functionality * Tests core YAML parsing and validation without complex dependencies */ import { describe, it, expect } from "vitest"; import { readFileSync } from "fs"; import { resolve } from "path"; import { load as yamlLoad } from "js-yaml"; import { z } from "zod"; // Simple schemas for testing const YamlToolParameterSchema = z.object({ name: z.string().min(1), type: z.enum(["string", "number", "boolean", "integer", "float", "array"]), description: z.string().optional(), default: z.any().optional(), required: z.boolean().optional(), itemType: z .enum(["string", "number", "boolean", "integer", "float"]) .optional(), min: z.number().optional(), max: z.number().optional(), minLength: z.number().optional(), maxLength: z.number().optional(), enum: z.array(z.union([z.string(), z.number(), z.boolean()])).optional(), pattern: z.string().optional(), }); const YamlSourceSchema = z.object({ host: z.string().min(1), user: z.string().min(1), password: z.string().min(1), port: z.number().int().positive(), "ignore-unauthorized": z.boolean().optional(), }); const YamlToolSchema = z.object({ source: z.string().min(1), description: z.string().min(1), statement: z.string().min(1), parameters: z.array(YamlToolParameterSchema).optional(), domain: z.string().optional(), category: z.string().optional(), metadata: z.record(z.any()).optional(), }); const YamlToolsetSchema = z.object({ tools: z.array(z.string().min(1)), }); const YamlToolsConfigSchema = z .object({ sources: z.record(z.string().min(1), YamlSourceSchema).optional(), tools: z.record(z.string().min(1), YamlToolSchema).optional(), toolsets: z.record(z.string().min(1), YamlToolsetSchema).optional(), metadata: z.record(z.any()).optional(), }) .refine( (data) => { return data.sources || data.tools || data.toolsets; }, { message: "YAML file must contain at least one section: sources, tools, or toolsets", }, ); // Helper function to load fixture function loadFixture(relativePath: string): string { const fixturePath = resolve(process.cwd(), "tests/fixtures", relativePath); return readFileSync(fixturePath, "utf-8"); } // Helper function to parse YAML function parseYaml(yamlContent: string) { try { const parsed = yamlLoad(yamlContent); const result = YamlToolsConfigSchema.safeParse(parsed); if (!result.success) { return { success: false, errors: result.error.errors.map( (e) => `${e.path.join(".")}: ${e.message}`, ), }; } return { success: true, config: result.data, }; } catch (error) { return { success: false, errors: [error instanceof Error ? error.message : "Parse error"], }; } } describe("YAML Parser Unit Tests", () => { describe("Optional Sections", () => { it("should parse sources-only file successfully", () => { const yamlContent = loadFixture("sources-only.yaml"); const result = parseYaml(yamlContent); expect(result.success).toBe(true); expect(result.config?.sources).toBeDefined(); expect(result.config?.tools).toBeUndefined(); expect(result.config?.toolsets).toBeUndefined(); }); it("should parse tools-only file successfully", () => { const yamlContent = loadFixture("tools-only.yaml"); const result = parseYaml(yamlContent); expect(result.success).toBe(true); expect(result.config?.sources).toBeUndefined(); expect(result.config?.tools).toBeDefined(); expect(result.config?.toolsets).toBeUndefined(); }); it("should parse toolsets-only file successfully", () => { const yamlContent = loadFixture("toolsets-only.yaml"); const result = parseYaml(yamlContent); expect(result.success).toBe(true); expect(result.config?.sources).toBeUndefined(); expect(result.config?.tools).toBeUndefined(); expect(result.config?.toolsets).toBeDefined(); }); it("should parse complete file successfully", () => { const yamlContent = loadFixture("complete.yaml"); const result = parseYaml(yamlContent); expect(result.success).toBe(true); expect(result.config?.sources).toBeDefined(); expect(result.config?.tools).toBeDefined(); expect(result.config?.toolsets).toBeDefined(); }); it("should reject empty file", () => { const yamlContent = loadFixture("invalid/empty.yaml"); const result = parseYaml(yamlContent); expect(result.success).toBe(false); expect( result.errors?.some( (error) => error.includes("YAML file must contain at least one section") || error.includes("Expected object, received null"), ), ).toBe(true); }); }); describe("Schema Validation", () => { it("should validate required fields in sources", () => { const yamlContent = loadFixture("invalid/missing-required-fields.yaml"); const result = parseYaml(yamlContent); expect(result.success).toBe(false); expect(result.errors).toBeDefined(); expect(result.errors?.some((error) => error.includes("user"))).toBe(true); expect(result.errors?.some((error) => error.includes("password"))).toBe( true, ); }); it("should validate parameter types", () => { const invalidParamConfig = ` tools: invalid-param-tool: source: test-source description: "Tool with invalid parameter" statement: "SELECT 1" parameters: - name: invalid-param type: invalid-type description: "Invalid parameter type" `; const result = parseYaml(invalidParamConfig); expect(result.success).toBe(false); expect( result.errors?.some((error) => error.includes("invalid-type")), ).toBe(true); }); it("should validate required tool fields", () => { const invalidToolConfig = ` tools: incomplete-tool: source: test-source # Missing description and statement `; const result = parseYaml(invalidToolConfig); expect(result.success).toBe(false); expect( result.errors?.some((error) => error.includes("description")), ).toBe(true); expect(result.errors?.some((error) => error.includes("statement"))).toBe( true, ); }); }); describe("Environment Variable Interpolation", () => { it("should handle environment variables in YAML", () => { // Set test environment variables process.env.TEST_HOST = "test-host.example.com"; process.env.TEST_USER = "test-user"; process.env.TEST_PASS = "test-password"; const yamlContent = ` sources: test-source: host: \${TEST_HOST} user: \${TEST_USER} password: \${TEST_PASS} port: 8076 `; // Simple environment variable interpolation const interpolatedContent = yamlContent.replace( /\$\{([^}]+)\}/g, (match, varName) => { return process.env[varName] || match; }, ); const result = parseYaml(interpolatedContent); expect(result.success).toBe(true); expect(result.config?.sources?.["test-source"]?.host).toBe( "test-host.example.com", ); expect(result.config?.sources?.["test-source"]?.user).toBe("test-user"); expect(result.config?.sources?.["test-source"]?.password).toBe( "test-password", ); // Clean up delete process.env.TEST_HOST; delete process.env.TEST_USER; delete process.env.TEST_PASS; }); }); describe("Parameter Validation", () => { it("should validate valid parameter types", () => { const validTypes = [ "string", "number", "boolean", "integer", "float", "array", ]; validTypes.forEach((type) => { const param = { name: "test-param", type: type as unknown, description: "Test parameter", }; const result = YamlToolParameterSchema.safeParse(param); expect(result.success).toBe(true); }); }); it("should reject invalid parameter types", () => { const invalidParam = { name: "test-param", type: "invalid-type", description: "Test parameter", }; const result = YamlToolParameterSchema.safeParse(invalidParam); expect(result.success).toBe(false); }); it("should require parameter name", () => { const paramWithoutName = { type: "string", description: "Test parameter", }; const result = YamlToolParameterSchema.safeParse(paramWithoutName); expect(result.success).toBe(false); }); it("should validate array parameters with itemType", () => { const arrayParam = { name: "ids", type: "array", itemType: "integer", description: "Array of user IDs", minLength: 1, maxLength: 100, }; const result = YamlToolParameterSchema.safeParse(arrayParam); expect(result.success).toBe(true); }); it("should validate parameters with validation constraints", () => { const constrainedParam = { name: "age", type: "integer", description: "User age", min: 0, max: 120, required: true, }; const result = YamlToolParameterSchema.safeParse(constrainedParam); expect(result.success).toBe(true); }); it("should validate parameters with enum values", () => { const enumParam = { name: "status", type: "string", description: "User status", enum: ["active", "inactive", "pending"], required: true, }; const result = YamlToolParameterSchema.safeParse(enumParam); expect(result.success).toBe(true); }); it("should validate parameters with pattern", () => { const patternParam = { name: "username", type: "string", description: "IBM i username", pattern: "^[A-Z0-9_]{1,10}$", required: true, }; const result = YamlToolParameterSchema.safeParse(patternParam); expect(result.success).toBe(true); }); }); describe("Tool Configuration Validation", () => { it("should parse tools with regular parameters", () => { const toolWithParams = ` tools: user-query: source: test-source description: "Query user information" statement: "SELECT * FROM users WHERE name = :username AND age > :minAge" parameters: - name: username type: string description: "User name to search for" required: true - name: minAge type: integer description: "Minimum age filter" min: 0 max: 120 default: 18 `; const result = parseYaml(toolWithParams); expect(result.success).toBe(true); expect(result.config?.tools?.["user-query"]?.parameters).toHaveLength(2); expect(result.config?.tools?.["user-query"]?.parameters?.[0].name).toBe( "username", ); expect(result.config?.tools?.["user-query"]?.parameters?.[0].type).toBe( "string", ); expect(result.config?.tools?.["user-query"]?.parameters?.[1].name).toBe( "minAge", ); expect(result.config?.tools?.["user-query"]?.parameters?.[1].type).toBe( "integer", ); }); it("should validate tools without any parameters", () => { const toolWithoutParams = ` tools: simple-query: source: test-source description: "Simple static query" statement: "SELECT COUNT(*) as total_users FROM users" `; const result = parseYaml(toolWithoutParams); expect(result.success).toBe(true); expect( result.config?.tools?.["simple-query"]?.parameters, ).toBeUndefined(); }); }); describe("Configuration Counts", () => { it("should count sources correctly", () => { const yamlContent = loadFixture("sources-only.yaml"); const result = parseYaml(yamlContent); expect(result.success).toBe(true); const sourceCount = Object.keys(result.config?.sources || {}).length; expect(sourceCount).toBe(3); }); it("should count tools correctly", () => { const yamlContent = loadFixture("tools-only.yaml"); const result = parseYaml(yamlContent); expect(result.success).toBe(true); const toolCount = Object.keys(result.config?.tools || {}).length; expect(toolCount).toBe(3); }); it("should count toolsets correctly", () => { const yamlContent = loadFixture("toolsets-only.yaml"); const result = parseYaml(yamlContent); expect(result.success).toBe(true); const toolsetCount = Object.keys(result.config?.toolsets || {}).length; expect(toolsetCount).toBe(3); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/IBM/ibmi-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server