Custom tools allow you to define reusable command-line tools directly in your YAML configuration. These tools can then be used in MCP (Model Context Protocol) blocks throughout your configuration, making it easy to integrate any command-line tool or script into your workflow.
- Define tools in YAML: No need to create separate scripts or programs
- Input validation: Define JSON Schema for tool parameters
- Template support: Use Liquid templates for dynamic command generation
- Transform outputs: Process tool output with Liquid templates or JavaScript
- Reusable: Define once, use multiple times across your configuration
- Importable: Share tools across projects using the
extendsmechanism - Type-safe: Full TypeScript support with input/output schemas
- MCP-compatible: Tools follow the Model Context Protocol specification
tools:
my-tool:
name: my-tool
description: Description of what the tool does
exec: 'echo "Hello World"'tools:
tool-name:
# MCP-compatible fields (these map directly to MCP tool interface)
name: tool-name # Required: Tool identifier (MCP: name)
description: Tool description # Recommended: Human-readable description (MCP: description)
# Input schema (JSON Schema format) - MCP: inputSchema
# This follows the JSON Schema specification and is used for:
# 1. Validating tool inputs before execution
# 2. Providing type information to AI models
# 3. Auto-generating documentation
inputSchema:
type: object
properties:
param1:
type: string
description: Parameter description # Describe each parameter for AI models
param2:
type: number
description: Optional parameter
required: [param1] # List required parameters
additionalProperties: false # Strict mode: reject unknown parameters
# Custom tool execution fields
exec: 'command {{ args.param1 }}' # Required: Command to execute (supports Liquid)
stdin: '{{ args.param2 }}' # Optional: Data to pipe to stdin (supports Liquid)
cwd: /path/to/directory # Optional: Working directory
env: # Optional: Environment variables
MY_VAR: value
timeout: 30000 # Optional: Timeout in milliseconds (default: 30000)
parseJson: true # Optional: Parse output as JSON
# Transform output with Liquid template
transform: '{ "result": {{ output | json }} }'
# OR transform with JavaScript
transform_js: |
return {
processed: output.trim().toUpperCase()
};
# Output schema for validation (optional) - MCP: outputSchema
# Not currently enforced but useful for documentation
outputSchema:
type: object
properties:
result:
type: string
description: The processed resultCustom tools are designed to be fully compatible with the Model Context Protocol (MCP) specification. When you define a custom tool, it automatically becomes available as an MCP tool with the following mapping:
| Custom Tool Field | MCP Tool Field | Purpose |
|---|---|---|
name |
name |
Unique identifier for the tool |
description |
description |
Human-readable description for AI models and documentation |
inputSchema |
inputSchema |
JSON Schema defining expected parameters |
outputSchema |
outputSchema |
JSON Schema for output validation (informational) |
- AI Model Integration: Tools with proper descriptions and schemas can be automatically understood and used by AI models
- Type Safety: Input schemas provide runtime validation and type checking
- Documentation: Schemas serve as self-documenting interfaces
- Interoperability: Tools can potentially be used with other MCP-compatible systems
-
Always provide descriptions: Help AI models understand what your tool does
tools: analyze-code: name: analyze-code description: "Analyzes source code for complexity metrics and potential issues"
-
Use detailed input schemas: Include descriptions for each parameter
inputSchema: type: object properties: file: type: string description: "Path to the source code file to analyze" metrics: type: array description: "List of metrics to calculate" items: type: string enum: ["complexity", "lines", "dependencies"] required: ["file"]
-
Consider output schemas: While not enforced, they document expected outputs
outputSchema: type: object properties: complexity: type: number description: "Cyclomatic complexity score" issues: type: array description: "List of detected issues"
Use ai_custom_tools to expose custom tools to AI providers via an ephemeral MCP server. See AI Custom Tools for complete documentation.
steps:
ai-review:
type: ai
prompt: |
Use the available tools to analyze the code for issues.
ai_custom_tools:
- grep-pattern
- file-stats
ai:
provider: anthropic
model: claude-3-5-sonnet-20241022Custom tools can also be used directly in MCP steps by setting transport: custom. The MCP provider supports four transport types:
stdio- Spawn an MCP server as a subprocess (default)sse- Connect to an MCP server via Server-Sent Events (legacy)http- Connect via Streamable HTTP transportcustom- Execute YAML-defined custom tools directly
steps:
my-check:
type: mcp
transport: custom # Use custom transport for YAML-defined tools
method: my-tool # Tool name (must be defined in tools: section)
methodArgs: # Tool arguments
param1: "value1"
param2: 42Tools have access to a rich template context through Liquid templates:
{{ args }}- The arguments passed to the tool{{ input }}- Alias forargs(same object){{ pr }}- Pull request information:{{ pr.number }}- PR number{{ pr.title }}- PR title{{ pr.author }}- PR author{{ pr.branch }}- Head branch name{{ pr.base }}- Base branch name
{{ files }}- List of files in the PR{{ outputs }}- Outputs from previous checks{{ env }}- Environment variables
- All of the above, plus:
{{ output }}- The raw command output (or parsed JSON ifparseJson: true){{ stdout }}- Standard output (raw string){{ stderr }}- Standard error (raw string){{ exitCode }}- Command exit code (number)
tools:
grep-todos:
name: grep-todos
description: Find TODO comments in code
inputSchema:
type: object
properties:
pattern:
type: string
files:
type: array
items:
type: string
exec: 'grep -n "{{ args.pattern }}" {{ args.files | join: " " }}'tools:
analyze-package:
name: analyze-package
description: Analyze package.json dependencies
inputSchema:
type: object
properties:
file:
type: string
exec: 'cat {{ args.file }}'
parseJson: true
transform_js: |
const deps = Object.keys(output.dependencies || {});
const devDeps = Object.keys(output.devDependencies || {});
return {
totalDeps: deps.length + devDeps.length,
prodDeps: deps.length,
devDeps: devDeps.length
};tools:
build-and-test:
name: build-and-test
description: Build project and run tests
exec: |
npm run build && npm test
timeout: 300000 # 5 minutes
transform_js: |
if (exitCode !== 0) {
return {
success: false,
error: stderr || 'Build or tests failed'
};
}
return {
success: true,
output: output
};tools:
flexible-linter:
name: flexible-linter
description: Run appropriate linter based on file type
inputSchema:
type: object
properties:
file:
type: string
exec: |
{% assign ext = args.file | split: "." | last %}
{% case ext %}
{% when "js", "ts" %}
eslint {{ args.file }}
{% when "py" %}
pylint {{ args.file }}
{% when "go" %}
golint {{ args.file }}
{% else %}
echo "No linter for .{{ ext }} files"
{% endcase %}You can expose an OpenAPI spec as MCP tools by defining a single reusable API bundle.
Both spec and overlays support file/URL and inline object forms:
tools:
petstore-api:
type: api
name: petstore-api
description: Petstore API as MCP tools
spec: ./petstore-openapi.yaml
overlays:
- ./petstore-overlay.yaml
whitelist:
- "get*"
- "POST:/pets*"
targetUrl: https://petstore.example.com
headers:
X-Api-Version: "2026-01"
Authorization: "Bearer ${PETSTORE_BEARER_TOKEN}"
X-Tenant-Id: "${PETSTORE_TENANT_ID}"
apiKey: "${PETSTORE_API_KEY}"
profiles-api:
type: api
name: profiles-api
spec:
openapi: "3.0.0"
info: { title: Profiles API, version: "1.0.0" }
servers: [{ url: "https://api.example.com" }]
paths:
/profiles/{id}:
get:
operationId: getProfile
parameters:
- name: id
in: path
required: true
schema: { type: string }
responses:
"200":
description: OK
overlays:
- ./profiles-overlay.yaml
- actions:
- target: "$.paths['/profiles/{id}'].get.operationId"
update: getProfileFromInlineOverlayBehavior:
- Each OpenAPI operation with an
operationIdis exposed as an MCP tool. - Tool names/descriptions come from OpenAPI and support
x-mcpoverrides. - Inputs include path/query/header parameters and
requestBody. - Security schemes from OpenAPI are applied at call time using
apiKey/securityCredentials. whitelist/blacklistsupports glob patterns foroperationIdandMETHOD:/path.- Overlay behavior matches
api-to-mcp: action-based overlays (actions[].target/update/remove) plus deep-merge overlays whenactionsis omitted. headersvalues can reference environment variables using the format${VARIABLE_NAME}(for example,${PETSTORE_BEARER_TOKEN}).
This works with ai_custom_tools, ai_mcp_servers.<name>.tools, and transport: custom MCP execution.
Runnable examples in this repo:
examples/api-tools-library.yaml(reusable API bundle in a separate file)examples/api-tools-mcp-example.yaml(includes embedded tests)examples/api-tools-ai-example.yaml(includes embedded tests)examples/api-tools-inline-overlay-example.yaml(includes embedded tests)
Create a file with just tool definitions:
# tools-library.yaml
version: "1.0"
tools:
tool1:
name: tool1
exec: 'command1'
tool2:
name: tool2
exec: 'command2'Use the extends mechanism to import tools:
version: "1.0"
extends: ./tools-library.yaml
# Additional tools can be defined here
tools:
local-tool:
name: local-tool
exec: 'local-command'
# Use both imported and local tools
steps:
check1:
type: mcp
transport: custom
method: tool1 # From tools-library.yaml
check2:
type: mcp
transport: custom
method: local-tool # Defined locallyYou can import from multiple sources:
extends:
- ./base-tools.yaml
- ./security-tools.yaml
- https://example.com/shared-tools.yamlTools are merged with later sources overriding earlier ones.
steps:
lint-all-files:
type: mcp
transport: custom
method: my-linter
forEach: "{{ files }}"
methodArgs:
file: "{{ item.filename }}"steps:
optional-check:
type: mcp
transport: custom
method: my-tool
if: "files.some(f => f.filename.endsWith('.js'))"
methodArgs:
target: "src/"steps:
main-check:
type: mcp
transport: custom
method: build-tool
on_success:
- type: mcp
transport: custom
method: test-tool
on_failure:
- type: mcp
transport: custom
method: cleanup-tool- Use Input Schemas: Always define
inputSchemato validate tool inputs - Handle Errors: Use
transform_jsto check exit codes and handle errors - Set Timeouts: Configure appropriate timeouts for long-running commands
- Parse JSON: Use
parseJson: truefor tools that output JSON - Document Tools: Provide clear descriptions for each tool
- Create Libraries: Group related tools in separate YAML files
- Version Control: Store tool libraries in version control for sharing
- Test Tools: Test tools independently before using in complex workflows
- Tools execute with the same permissions as the Visor process
- Be cautious with user input in tool commands
- Use input validation to prevent command injection
- Avoid exposing sensitive data in tool outputs
- Consider using environment variables for secrets
If you get "Tool not found" errors:
- Ensure the tool is defined in the
toolssection - Check that the tool name matches exactly
- Verify extends paths are correct
For command execution issues:
- Test the command manually first
- Check working directory (
cwd) settings - Verify required binaries are installed
- Check timeout settings for long operations
For Liquid template problems:
- Validate template syntax
- Check that variables exist in context
- Use filters correctly (e.g.,
| json,| join)
For JavaScript transform issues:
- Ensure valid JavaScript syntax
- Always return a value
- Handle undefined/null cases
- Use try-catch for error handling