definePlugin() and defineTool() — that give you full TypeScript inference from input schema to handler return type.
Installation
Defining a tool
defineTool() is a thin typed wrapper around a ToolConfig object. It exists to give you inference and autocompletion when authoring a tool in isolation, before you pass it to definePlugin().
ToolConfig fields
| Field | Type | Required | Description |
|---|---|---|---|
path | string | Yes | Dot-separated tool identifier, e.g. github.issues.list |
name | string | Yes | Human-readable display name |
description | string | Yes | What the tool does, when to use it, and what it returns |
inputSchema | z.ZodType | Yes | Zod schema for the tool’s input |
outputSchema | z.ZodType | No | Zod schema for the tool’s output |
destructive | boolean | No | true if the tool modifies or deletes state (default false) |
authProvider | string | No | ID of the auth provider this tool uses |
serviceAccountAllowed | boolean | No | Whether a service account may invoke this tool without a user token (default false) |
handler | function | Yes | The function that runs when the tool is invoked |
Tool naming
Tool paths follow thenamespace.resource.verb pattern. Every segment is lowercase with underscores for multi-word segments, and the full path must match ^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$.
github.issues.create is better than github.create_issue because it groups related tools under the same subtree, which policy patterns can then address with github.issues.*.
Tool annotations
Annotations are hints to MCP clients about how a tool behaves. Set them directly on the tool config:destructive
destructive
Set
destructive: true on any tool that deletes, overwrites, or sends data. MCP clients use this to decide whether to auto-approve invocations or prompt the user. Toolshed also uses it to decide whether the tool’s handler must call ctx.elicit() before proceeding.Read-only (no annotation needed)
Read-only (no annotation needed)
Tools without
destructive: true are treated as read-only by default. MCP clients may auto-approve them. You don’t need to set any flag — the absence of destructive is enough.serviceAccountAllowed
serviceAccountAllowed
Set
serviceAccountAllowed: true if the tool can run on behalf of a service account rather than a specific user. This is useful for background jobs that don’t have an interactive user token.Defining a plugin
Once you have your tools, pass them todefinePlugin() to produce a Plugin object you can register with the server.
PluginConfig fields
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Stable unique identifier for the plugin |
name | string | Yes | Human-readable plugin name |
description | string | Yes | What the plugin provides |
authProviders | AuthProvider[] | Yes | Auth providers the plugin’s tools may use |
tools | ToolConfig[] | Yes | The tools this plugin exposes |
Using PluginContext in handlers
Every handler receives aPluginContext as its first argument. It provides three capabilities:
ctx.auth.getToken()
Retrieve a user’s OAuth token or API key for a named provider. The provider ID must match one of theauthProviders declared in your PluginConfig.
ctx.elicit()
Suspend execution and ask the user to approve or provide additional input. Always call this before performing a destructive mutation.type field accepts "approval" (yes/no confirmation) or "form" (structured data collection). The response object contains approved: boolean and an optional data map for form responses.