Skip to content

Defining Tools

Z-M-Huang edited this page Mar 19, 2026 · 3 revisions

Defining Tools

Tools are the primary interface for MCP clients. Each tool has a name, description, input schema, and an action script that runs when the tool is called.

Tool Schema

tools:
  - name: my-tool              # Required. 1-64 chars, [a-zA-Z0-9_-]+
    title: My Tool              # Optional. Display name for UIs
    description: |              # Required. Tells the AI when/how to use this tool
      Does X when the user asks about Y.
      Returns Z in markdown format.
    inputSchema:                # Required. JSON Schema (type: object)
      type: object
      properties:
        param1:
          type: string
          description: What this parameter is for
      required: [param1]
    action: https://example.com/actions/my-tool.v1.js    # Required. URL or local file path
    # Or for local development:
    # action: ./actions/my-tool.v1.js
    actionHash: "sha256:64_hex_chars_here"                # Required
    annotations:                # Optional. Hints for MCP clients
      readOnlyHint: true
      destructiveHint: false
      idempotentHint: true
      openWorldHint: false

Input Schema

The inputSchema follows JSON Schema with type: object at the root.

Note: At runtime, git-doc-mcp validates type and required fields. Additional JSON Schema features (enum, default, minLength, etc.) are passed through to MCP clients as hints for AI assistants but are not enforced by the server. MCP clients use these hints to generate better tool calls.

Supported Property Types

String:

properties:
  name:
    type: string
    description: User's name

String with enum (hint for MCP clients, not enforced at runtime):

properties:
  format:
    type: string
    description: "Output format (json, csv, text, or markdown)"
    enum: [json, csv, text, markdown]

Number:

properties:
  limit:
    type: number
    description: Maximum number of results (1-100)

Boolean:

properties:
  include_details:
    type: boolean
    description: Whether to include full details

String with default (hint for MCP clients, not enforced at runtime):

properties:
  branch:
    type: string
    description: "Git branch name (default: main)"
    default: main

Required vs Optional Parameters

inputSchema:
  type: object
  properties:
    query:                       # Required — listed in 'required' array
      type: string
      description: Search query
    language:                    # Optional — not in 'required' array
      type: string
      description: Filter by programming language
    max_results:                 # Optional with default
      type: number
      description: Max results to return (default 10)
  required: [query]              # Only 'query' is required

Complex Input Examples

Multiple required parameters:

inputSchema:
  type: object
  properties:
    owner:
      type: string
      description: Repository owner
    repo:
      type: string
      description: Repository name
    path:
      type: string
      description: File path within the repository
  required: [owner, repo, path]

All optional parameters:

inputSchema:
  type: object
  properties:
    tag:
      type: string
      description: Filter by tag
    sort:
      type: string
      description: Sort order
      enum: [name, date, relevance]
  # No 'required' field — all parameters are optional

No parameters:

inputSchema:
  type: object
  properties: {}
  # Tool takes no input

Tool Annotations

Annotations are hints that help MCP clients understand tool behavior. They don't affect execution.

annotations:
  readOnlyHint: true       # Tool only reads data, doesn't modify anything
  destructiveHint: false   # Tool doesn't delete or irreversibly change data
  idempotentHint: true     # Calling multiple times with same input gives same result
  openWorldHint: true      # Tool accesses external services (internet)
Annotation Default Meaning when true
readOnlyHint false Tool only reads, no side effects
destructiveHint false Tool may delete or irreversibly change data
idempotentHint false Same input always produces same result
openWorldHint false Tool accesses external/internet resources

Annotation Patterns

Read-only data fetcher:

annotations:
  readOnlyHint: true
  destructiveHint: false
  idempotentHint: true
  openWorldHint: true

Search tool (results may change over time):

annotations:
  readOnlyHint: true
  destructiveHint: false
  idempotentHint: false    # Results may differ between calls
  openWorldHint: true

Data modification tool:

annotations:
  readOnlyHint: false
  destructiveHint: false   # Modifies but doesn't destroy
  idempotentHint: true     # PUT-like: same input, same result
  openWorldHint: true

Deletion tool:

annotations:
  readOnlyHint: false
  destructiveHint: true    # Irreversible
  idempotentHint: true     # Deleting twice has same effect
  openWorldHint: true

Action Hashes

Every tool requires a SHA256 hash of its action script for integrity verification.

Computing the Hash

# macOS/Linux
shasum -a 256 actions/my-tool.v1.js
# Output: a1b2c3d4e5f6... actions/my-tool.v1.js

# Using openssl
openssl dgst -sha256 actions/my-tool.v1.js

# Using Node.js
node -e "const fs=require('fs'); const crypto=require('crypto'); console.log('sha256:'+crypto.createHash('sha256').update(fs.readFileSync('actions/my-tool.v1.js')).digest('hex'))"

Hash Format

actionHash: "sha256:a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890"

Format: sha256: followed by exactly 64 lowercase hexadecimal characters.

When to Update Hashes

You must recompute and update the hash whenever the action script changes. Even a single character change produces a completely different hash. This is by design — it ensures the action code running is exactly what you reviewed.

Tool Naming

Rules

  • 1 to 64 characters
  • Allowed: letters, digits, underscores, hyphens ([a-zA-Z0-9_-]+)
  • Case-sensitive

Conventions

# Good: descriptive, kebab-case
- name: fetch-file
- name: search-code
- name: list-issues
- name: get-user-profile

# Good: with namespace prefix
- name: github-search
- name: jira-get-ticket
- name: slack-send-message

# Avoid: too generic
- name: get          # Get what?
- name: data         # What kind?
- name: do-thing     # Not descriptive

Writing Good Descriptions

The description field is critical — it's what AI assistants use to decide when to call your tool.

Good Descriptions

description: |
  Search for code patterns across all files in a GitHub repository.
  Returns matching file paths with line numbers and code snippets.
  Supports regex patterns and language filters.
  Use this when the user asks to find specific code, function definitions,
  or usage patterns in a repository.
description: |
  Fetch the raw content of a file from a GitHub repository.
  Returns the file content as plain text.
  Use this to read source code, documentation, or configuration files.
  Requires owner, repo, and file path.

Bad Descriptions

# Too vague — AI won't know when to use it
description: Gets data from the API

# Too implementation-focused — should describe purpose, not internals
description: Makes a GET request to /api/v2/search endpoint with query parameters

# Missing context about when to use it
description: Returns JSON data

Writing Descriptions That LLMs Act On

The examples above are a good start, but there's a critical pattern that dramatically increases how often AI assistants call your tools: explicit trigger conditions.

LLMs decide whether to call a tool by matching the user's intent against the tool description. A description that says "searches code" is informative. A description that says "USE THIS WHEN the user asks to find specific code" is actionable.

The trigger pattern:

description: |
  Search for code patterns across all files in a GitHub repository.
  Returns matching file paths with line numbers and code snippets.
  Supports regex patterns and language filters.

  USE THIS WHEN:
  - The user asks to find specific functions, imports, or usage patterns
  - The user says "search for", "find", "where is", or "grep"
  - The user wants to understand how a feature is implemented
  - You need to locate code before making changes

  DO NOT USE WHEN:
  - You already know the exact file path (use get-file instead)
  - The user wants to browse directory structure (use list-files instead)

  Example: "Find all uses of fetchData" / "Where is the auth middleware?" / "Search for TODO comments"

What each section does:

Section Purpose
First paragraph What the tool does and what it returns
USE THIS WHEN: Explicit conditions that should trigger tool invocation
DO NOT USE WHEN: Prevents misuse and points to the right alternative
Example: Concrete user queries the LLM can pattern-match against

Before/after comparison:

# BEFORE: LLM must infer when to use this
description: Gets data from the API. Returns JSON.

# AFTER: LLM knows exactly when and when not to use this
description: |
  Fetch user profile data from the Acme API.
  Returns the user's name, email, role, and last login as JSON.

  USE THIS WHEN:
  - The user asks about a specific person ("who is john?", "show me user 42")
  - The user needs account details, permissions, or login history
  - You need to verify a user exists before performing other operations

  DO NOT USE WHEN:
  - The user wants to search across all users (use search-users instead)
  - The user wants to modify a profile (use update-user instead)

  Example: "Get the profile for user alice" / "What role does user 12 have?"

Multiple Tools Example

A manifest with multiple tools covering different use cases:

tools:
  - name: list-documents
    title: List Documents
    description: |
      List all documents in a workspace. Optionally filter by tag or type.
      Returns document titles, IDs, and last modified dates.
    inputSchema:
      type: object
      properties:
        tag:
          type: string
          description: Filter by tag name
        type:
          type: string
          description: Filter by document type
          enum: [note, article, template, report]
      # All optional — lists everything if no filters
    action: https://example.com/actions/list-docs.v1.js
    actionHash: "sha256:..."
    annotations:
      readOnlyHint: true
      idempotentHint: false    # List may change between calls
      openWorldHint: true

  - name: get-document
    title: Get Document
    description: |
      Fetch the full content of a document by ID.
      Returns the document body in markdown format with metadata.
    inputSchema:
      type: object
      properties:
        id:
          type: string
          description: Document ID
      required: [id]
    action: https://example.com/actions/get-doc.v1.js
    actionHash: "sha256:..."
    annotations:
      readOnlyHint: true
      idempotentHint: true
      openWorldHint: true

  - name: search-documents
    title: Search Documents
    description: |
      Full-text search across all documents in the workspace.
      Returns matching documents ranked by relevance with highlighted snippets.
    inputSchema:
      type: object
      properties:
        query:
          type: string
          description: Search keywords or phrase
        max_results:
          type: number
          description: Maximum results to return (default 10, max 50)
      required: [query]
    action: https://example.com/actions/search-docs.v1.js
    actionHash: "sha256:..."
    annotations:
      readOnlyHint: true
      idempotentHint: false
      openWorldHint: true

Clone this wiki locally