Step-by-step guide to creating custom YAML SQL tools for IBM i database operations with examples and best practices.
This guide walks you through creating custom YAML SQL tools from basic queries to advanced enterprise operations. You’ll learn to build tools that integrate seamlessly with AI agents while maintaining IBM i security and performance standards.
Prerequisites: Basic SQL knowledge and familiarity with IBM i system services (QSYS2 schema). For YAML syntax, any text editor will work, but one with YAML syntax highlighting is recommended.
# Database connection configurationsources: ibmi-system: host: ${DB2i_HOST} user: ${DB2i_USER} password: ${DB2i_PASS} port: 8076 ignore-unauthorized: true # Development only# Define your SQL toolstools: list_active_jobs: source: ibmi-system description: "List currently active jobs on the IBM i system" statement: | SELECT job_name, user_name, job_status, cpu_used, elapsed_time FROM qsys2.active_job_info WHERE job_status = 'ACTIVE' ORDER BY cpu_used DESC FETCH FIRST 20 ROWS ONLY parameters: []# Organize tools into logical groupstoolsets: basic_monitoring: title: "Basic System Monitoring" description: "Simple tools for monitoring system activity" tools: - list_active_jobs
TOOLS_YAML_PATH=my-first-tools.yaml npm run start:http
Use the MCP Inspector to test your tool:
Copy
npm run mcp-inspector
Development Tip: Always start with simple, parameter-free tools to verify your connection and SQL syntax. You can add complexity incrementally as you gain confidence with the YAML structure.
tools: list_jobs_by_user: source: ibmi-system description: "List active jobs for a specific user" parameters: - name: user_name type: string required: true description: "IBM i user profile name (uppercase)" pattern: "^[A-Z][A-Z0-9_]*$" maxLength: 10 statement: | SELECT job_name, job_status, cpu_used, elapsed_time FROM qsys2.active_job_info WHERE user_name = :user_name AND job_status = 'ACTIVE' ORDER BY cpu_used DESC FETCH FIRST 50 ROWS ONLY
Copy
tools: flexible_job_search: source: ibmi-system description: "Search jobs with optional filtering" parameters: - name: user_filter type: string required: false description: "Optional user name filter" - name: max_rows type: integer default: 25 minimum: 1 maximum: 100 description: "Maximum number of jobs to return" statement: | SELECT job_name, user_name, job_status, cpu_used FROM qsys2.active_job_info WHERE (:user_filter IS NULL OR user_name = :user_filter) AND job_status = 'ACTIVE' ORDER BY cpu_used DESC FETCH FIRST :max_rows ROWS ONLY
Copy
tools: jobs_by_status: source: ibmi-system description: "List jobs filtered by job status" parameters: - name: status type: string enum: ["ACTIVE", "ENDED", "OUTQ", "MSGW"] default: "ACTIVE" description: "Job status to filter by" statement: | SELECT job_name, user_name, job_status, job_type, subsystem, elapsed_time FROM qsys2.active_job_info WHERE job_status = :status ORDER BY elapsed_time DESC FETCH FIRST 30 ROWS ONLY
The MCP server automatically validates parameters before executing SQL:
Copy
parameters: - name: library_name type: string required: true pattern: "^[A-Z][A-Z0-9_]*$" # IBM i naming rules maxLength: 10 description: "IBM i library name (must be uppercase alphanumeric)" - name: row_limit type: integer minimum: 1 maximum: 1000 default: 50 description: "Number of rows to return (1-1000)"
Security Note: Parameter validation is your first line of defense against invalid input. Always include appropriate constraints, especially for string parameters that could be used in injection attacks.
The tools/sample/employee-info.yaml file demonstrates a comprehensive set of tools using the IBM i SAMPLE schema. This example showcases all five parameter types, validation patterns, and SQL techniques in a single, production-ready configuration.
Why This Example Matters: Rather than showing isolated snippets, this complete file demonstrates how real-world tools are structured, how parameters work together, and how SQL patterns combine to create powerful data operations.
File Location:tools/sample/employee-info.yamlPurpose: Provide HR and project management capabilities using IBM i’s SAMPLE database (EMPLOYEE, DEPARTMENT, PROJECT tables)What You’ll Learn:
All 5 parameter types (string, integer, float, boolean, array) in production context
Source Reuse: This source is reused by all 8 tools in the file. Define sources once at the top, reference them in each tool using the source field. This ensures consistent connection settings and simplifies credential management.
Tool:get_employee_detailsDemonstrates: String pattern validation, table joins, self-joins for hierarchical data
Copy
get_employee_details: source: ibmi-sample description: Retrieve detailed information about an employee including department and manager statement: | SELECT E.EMPNO, E.FIRSTNME, E.MIDINIT, E.LASTNAME, E.JOB, E.HIREDATE, E.SALARY, E.BONUS, E.WORKDEPT, D.DEPTNAME, D.LOCATION, M.FIRSTNME AS MGR_FIRSTNME, M.LASTNAME AS MGR_LASTNAME FROM SAMPLE.EMPLOYEE E LEFT JOIN SAMPLE.DEPARTMENT D ON E.WORKDEPT = D.DEPTNO LEFT JOIN SAMPLE.EMPLOYEE M ON D.MGRNO = M.EMPNO WHERE E.EMPNO = :employee_id parameters: - name: employee_id type: string description: "Employee ID (e.g., '000010') - Must be 6 digits" required: true pattern: "^[0-9]{6}$"
Tools:find_employees_by_department, find_employees_by_jobDemonstrates: Enum constraints for controlled value selection
Department Filter
Job Title Filter
Copy
find_employees_by_department: source: ibmi-sample description: List employees in a specific department statement: | SELECT E.EMPNO, E.FIRSTNME, E.MIDINIT, E.LASTNAME, E.JOB, E.HIREDATE, E.SALARY FROM SAMPLE.EMPLOYEE E WHERE E.WORKDEPT = :department_id ORDER BY E.LASTNAME, E.FIRSTNME parameters: - name: department_id type: string description: "Department ID - Select from predefined departments" required: true enum: ["A00", "B01", "C01", "D01", "E01"]
Copy
find_employees_by_job: source: ibmi-sample description: Find employees with a specific job title statement: | SELECT E.EMPNO, E.FIRSTNME, E.MIDINIT, E.LASTNAME, E.WORKDEPT, D.DEPTNAME, E.HIREDATE, E.SALARY FROM SAMPLE.EMPLOYEE E LEFT JOIN SAMPLE.DEPARTMENT D ON E.WORKDEPT = D.DEPTNO WHERE E.JOB = :job_title ORDER BY E.LASTNAME, E.FIRSTNME parameters: - name: job_title type: string description: "Job title - Select from common job titles" required: true enum: ["MANAGER", "ANALYST", "DESIGNER", "CLERK", "SALESREP", "PRES"]
Enum Parameters: Enum parameters automatically enhance descriptions for LLMs: “Must be one of: ‘A00’, ‘B01’, ‘C01’…”. This provides autocomplete-like guidance and prevents invalid queries. Use enums whenever you have a fixed set of valid values.
Tool:get_employee_projectsDemonstrates: Boolean flags for conditional filtering, complex multi-table joins
Copy
get_employee_projects: source: ibmi-sample description: List projects an employee is working on statement: | SELECT P.PROJNO, P.PROJNAME, A.ACTNO, A.ACTDESC, EPA.EMSTDATE AS START_DATE, EPA.EMENDATE AS END_DATE, EPA.EMPTIME FROM SAMPLE.EMPPROJACT EPA JOIN SAMPLE.PROJECT P ON EPA.PROJNO = P.PROJNO JOIN SAMPLE.PROJACT PA ON EPA.PROJNO = PA.PROJNO AND EPA.ACTNO = PA.ACTNO JOIN SAMPLE.ACT A ON EPA.ACTNO = A.ACTNO WHERE EPA.EMPNO = :employee_id AND (:include_completed = 1 OR EPA.EMENDATE IS NULL) ORDER BY EPA.EMSTDATE DESC parameters: - name: employee_id type: string description: "Employee ID (e.g., '000010') - Must be 6 digits" required: true pattern: "^[0-9]{6}$" - name: include_completed type: boolean description: "Include completed projects (true) or only active projects (false)" default: true
Key Techniques:
Boolean in SQL::include_completed = 1 (true) or = 0 (false)
Conditional filtering:(:include_completed = 1 OR EPA.EMENDATE IS NULL) filters active projects when false
4-table joins: Connects employee project activities with projects, project activities, and activity descriptions
Default value: Makes parameter optional (defaults to showing all projects)
Usage Examples:
Copy
// Get all projects (completed and active){ "name": "get_employee_projects", "arguments": { "employee_id": "000010", "include_completed": true }}// Get only active projects{ "name": "get_employee_projects", "arguments": { "employee_id": "000010", "include_completed": false }}
get_department_salary_stats: source: ibmi-sample description: Salary statistics by department with optional salary range filter statement: | SELECT D.DEPTNO, D.DEPTNAME, COUNT(E.EMPNO) AS EMPLOYEE_COUNT, AVG(E.SALARY) AS AVG_SALARY, MIN(E.SALARY) AS MIN_SALARY, MAX(E.SALARY) AS MAX_SALARY, SUM(E.SALARY) AS TOTAL_SALARY FROM SAMPLE.DEPARTMENT D LEFT JOIN SAMPLE.EMPLOYEE E ON D.DEPTNO = E.WORKDEPT WHERE (D.DEPTNO = :department_id OR :department_id = '*ALL') AND (E.SALARY >= :min_salary OR :min_salary IS NULL) AND (E.SALARY <= :max_salary OR :max_salary IS NULL) GROUP BY D.DEPTNO, D.DEPTNAME ORDER BY D.DEPTNO parameters: - name: department_id type: string description: "Department ID (e.g., 'A00') or '*ALL' for all departments" default: "*ALL" - name: min_salary type: integer description: "Minimum salary filter" min: 0 max: 100000 default: 0 - name: max_salary type: integer description: "Maximum salary filter" min: 0 max: 100000 default: 100000
Aggregation functions: COUNT, AVG, MIN, MAX, SUM provide statistical summaries
GROUP BY: Groups results by department for aggregate calculations
Special value pattern:'*ALL' provides “all departments” option
NULL handling:OR :min_salary IS NULL allows optional filtering
Default Values: Integer parameters with default values don’t need required: false. The presence of a default makes them optional automatically. This pattern works for all parameter types.
Tool:find_project_team_membersDemonstrates: Array parameters with SQL IN clauses, array length constraints
Copy
find_project_team_members: source: ibmi-sample description: Find all employees working on specific projects statement: | SELECT E.EMPNO, E.FIRSTNME, E.MIDINIT, E.LASTNAME, E.JOB, E.WORKDEPT, D.DEPTNAME, EPA.PROJNO, EPA.EMSTDATE AS PROJECT_START_DATE, EPA.EMENDATE AS PROJECT_END_DATE, EPA.EMPTIME AS TIME_ALLOCATION FROM SAMPLE.EMPPROJACT EPA JOIN SAMPLE.EMPLOYEE E ON EPA.EMPNO = E.EMPNO LEFT JOIN SAMPLE.DEPARTMENT D ON E.WORKDEPT = D.DEPTNO WHERE EPA.PROJNO IN (:project_ids) ORDER BY EPA.PROJNO, E.LASTNAME, E.FIRSTNME parameters: - name: project_ids type: array itemType: string description: "List of project IDs to search for (e.g., ['MA2100', 'AD3100'])" required: true minLength: 1 maxLength: 10
Key Techniques:
Array expansion:IN (:project_ids) automatically expands to IN (?, ?, ?) with safe parameter binding
tools: system_overview: source: ibmi-system description: "Comprehensive system status and configuration" statement: | SELECT system_name, current_timestamp, elapsed_time, total_jobs, active_threads, cpu_utilization, main_storage_size / 1024 / 1024 AS memory_gb, current_unprotected_used / 1024 / 1024 AS used_memory_gb FROM TABLE(QSYS2.SYSTEM_STATUS()) X subsystem_status: source: ibmi-system description: "Status of all subsystems" statement: | SELECT subsystem_name, subsystem_library, status, monitor_job, secondary_language, COALESCE(current_active_jobs, 0) as active_jobs, COALESCE(maximum_active_jobs, 0) as max_jobs FROM qsys2.subsystem_info ORDER BY subsystem_name
Build robust tools with comprehensive error handling:
Copy
tools: validated_object_query: source: ibmi-system description: "Query objects with comprehensive validation" parameters: - name: library type: string required: true pattern: "^[A-Z][A-Z0-9_]*$" maxLength: 10 description: "Library name (must exist and be accessible)" - name: object_filter type: string required: false pattern: "^[A-Z*][A-Z0-9_*]*$" maxLength: 10 description: "Object name filter (supports * wildcards)" statement: | WITH library_check AS ( SELECT library_name FROM qsys2.library_info WHERE library_name = :library ), object_list AS ( SELECT o.*, l.library_name FROM library_check l, table(qsys2.object_statistics(l.library_name, COALESCE(:object_filter, '*ALL'))) o ) SELECT CASE WHEN NOT EXISTS(SELECT 1 FROM library_check) THEN 'ERROR: Library ' || :library || ' not found or not accessible' ELSE object_name END AS result, object_type, object_size, created_timestamp FROM object_list UNION ALL SELECT 'ERROR: Library ' || :library || ' not found or not accessible', NULL, NULL, NULL FROM library_check WHERE NOT EXISTS(SELECT 1 FROM library_check) ORDER BY result
# Validate YAML syntax and structurenpm run validate -- --config my-tools.yaml# List available toolsetsnpm run start:http -- --list-toolsets --tools my-tools.yaml# Test specific tool executionnpm run test -- --grep "yaml-sql"
For complete testing strategies including MCP Inspector usage, see the Quick Start Guide. For production testing, refer to the Production Deployment guide.
Organize tools by business domain or functional area:
Copy
my-tools/├── performance.yaml # System performance monitoring├── security.yaml # Security and audit tools├── business.yaml # Business logic and reporting└── development.yaml # Development and testing tools
Tool Design Philosophy: Effective SQL tools balance three concerns: usability (clear parameters and descriptions for AI agents), security (proper validation and authority checking), and performance (efficient queries that respect system resources). Start simple and add complexity incrementally as you understand your specific use cases.