Advanced

Build a custom tool

Ship an MCP server exposing your tool — starts at 10 lines.

Two packaging paths

Custom tools ship two ways: an in-process TypeScript bundle dropped into .kition/tools/ and loaded by the built-in runtime at launch, or a standalone MCP server (any language) exposed over stdio / SSE. Embed when the tool is small and pure; reach for MCP when you need state, long-lived connections, or reuse across vaults.

Both look identical to the agent — same tool schema surface, same permission model, same hook interception. The only real difference is distribution: embedded tools travel with the vault, MCP servers travel with the host.

From a UX standpoint, embedded tools are the shortest path to "write once, use immediately". MCP is the right answer when a tool needs to be shared across vaults, machines, or even teams.

Embedded tool skeleton

Save the file below as .kition/tools/word-count.ts. Kition scans, type-checks and registers it on launch. The default export must be defineTool({...}) — Kition uses the schema for argument validation and run for execution.

The ctx parameter exposes the logger, vault filesystem, session metadata, and secret store — you never need to require Kition internals directly.

import { defineTool } from '@kition/tools'
import { z } from 'zod'

export default defineTool({
  name: 'word_count',
  title: 'Word count',
  description: 'Count words in a string, optionally splitting on punctuation.',
  input: z.object({
    text: z.string().min(1),
    splitOnPunctuation: z.boolean().default(false),
  }),
  output: z.object({
    words: z.number(),
    chars: z.number(),
  }),
  async run({ input, ctx }) {
    const re = input.splitOnPunctuation ? /[\s\p{P}]+/u : /\s+/u
    const words = input.text.trim().split(re).filter(Boolean).length
    ctx.log.info('counted', { words })
    return { words, chars: input.text.length }
  },
})

Reaching vault and secrets

Non-trivial tools usually need to read vault notes, call external APIs, or persist state. The ctx object surfaces all of that — file I/O via ctx.vault, secrets via ctx.secrets (Keychain / DPAPI / libsecret), HTTP via ctx.fetch (auto-routed and auto-retried).

import { defineTool } from '@kition/tools'
import { z } from 'zod'

export default defineTool({
  name: 'jira_create_issue',
  title: 'Create Jira issue',
  description: 'Create a Jira issue from the current note.',
  input: z.object({
    project: z.string(),
    summary: z.string(),
    notePath: z.string(),
  }),
  async run({ input, ctx }) {
    const body = await ctx.vault.readFile(input.notePath)
    const token = await ctx.secrets.get('jira.api_token')
    const res = await ctx.fetch('https://acme.atlassian.net/rest/api/3/issue', {
      method: 'POST',
      headers: {
        authorization: `Basic ${token}`,
        'content-type': 'application/json',
      },
      body: JSON.stringify({
        fields: {
          project: { key: input.project },
          summary: input.summary,
          description: body,
          issuetype: { name: 'Task' },
        },
      }),
    })
    if (!res.ok) throw new Error(`jira ${res.status}`)
    const json = await res.json() as { key: string }
    return { key: json.key, url: `https://acme.atlassian.net/browse/${json.key}` }
  },
})

Standalone MCP server

Reach for MCP when the tool needs external APIs, a database connection, or a language other than TypeScript. Any server that speaks the Model Context Protocol can be mounted. The minimal TypeScript example below wires up the two core handlers, tools/list and tools/call.

For production we recommend stdio transport — fast startup, strong isolation, no port to manage. SSE makes sense when the server runs remotely (e.g. an internal tools service).

import { Server } from '@modelcontextprotocol/sdk/server'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio'

const server = new Server({ name: 'word-count', version: '0.1.0' })

server.setRequestHandler('tools/list', () => ({
  tools: [{
    name: 'word_count',
    description: 'Count words in a string',
    inputSchema: {
      type: 'object',
      properties: { text: { type: 'string' } },
      required: ['text'],
    },
  }],
}))

server.setRequestHandler('tools/call', async (req) => {
  const text = req.params.arguments.text as string
  const words = text.trim().split(/\s+/).filter(Boolean).length
  return { content: [{ type: 'text', text: String(words) }] }
})

await server.connect(new StdioServerTransport())

Register with Kition

Embedded tools need no config — drop the file in and it loads. MCP servers need a stdio entry in .kition/mcp.json; restart the agent and the tool shows up in the /tools listing.

Want everyone on the team running the same toolset? Commit .kition/mcp.json to the vault’s git repo — pulling is enough, the whole config is declarative.

{
  "servers": {
    "word-count": {
      "type": "stdio",
      "command": "node",
      "args": ["./scripts/word-count-server.js"],
      "env": { "LOG_LEVEL": "info" }
    },
    "internal-tools": {
      "type": "sse",
      "url": "https://tools.internal.acme.com/mcp",
      "headers": { "authorization": "Bearer ${env:INTERNAL_TOOLS_TOKEN}" }
    }
  }
}

Permissions and hooks

  • Tools default to "ask" — first invocation prompts the user; switch to allow / deny in settings
  • Use permissions.tools[name] for fine-grained control (e.g. network.fetch scoped to a domain)
  • Hooks can fire on pre_tool_use / post_tool_use for compliance, audit, redaction
  • Throw or return non-zero on failure — the agent sees the full stack trace
  • Every invocation is logged to .kition/audit.jsonl with input, output, and duration

Debugging tips

  • kition tools list prints what is loaded for the current vault and where it came from
  • kition tools call word_count --json '{"text":"hi there"}' invokes the tool directly, bypassing the agent
  • MCP server stderr lands in ~/Library/Logs/Kition/mcp-*.log
  • Schema mismatches show as clear zod / ajv errors at boot, not silent skips
  • Sprinkle ctx.log.debug(...) inside tools — surface with KITION_LOG_LEVEL=debug

Related articles

Ready when you are.

Kition is a local-first AI workspace. Markdown documents, structured tables, and an AI agent — running on your own machine, against the model provider you choose.