Skip to main content
A plugin groups related tools with shared authentication. This guide walks through building one from scratch, using the GitHub plugin as a reference.

Plugin structure

Every plugin exports a Plugin object created by definePlugin:
import { definePlugin, defineTool } from "@toolshed/sdk";
import { z } from "zod";

const myTool = defineTool({
  path: "my_service.items.list",
  name: "List Items",
  description: "List items from My Service.",
  inputSchema: z.object({
    limit: z.number().int().positive().default(10),
  }),
  outputSchema: z.object({
    items: z.array(z.object({
      id: z.string(),
      name: z.string(),
    })),
  }),
  async handler(ctx, input) {
    const token = await ctx.auth.getToken("my-service");
    const res = await fetch(`https://api.myservice.com/items?limit=${input.limit}`, {
      headers: { Authorization: `Bearer ${token}` },
    });
    const data = await res.json();
    return { items: data };
  },
});

const plugin = definePlugin({
  id: "my-service",
  name: "My Service",
  description: "Tools for My Service.",
  authProviders: [
    {
      id: "my-service",
      type: "oauth2",
      provider: "my-service",
      scopes: ["read", "write"],
    },
  ],
  tools: [myTool],
});

export default plugin;

Tool definition

Each tool is defined with defineTool:
FieldTypeRequiredDescription
pathstringYesDot-separated path (e.g., github.issues.create). See Tool Paths.
namestringYesTitle Case display name (e.g., “Create Issue”)
descriptionstringYesWhat the tool does
inputSchemaZod schemaYesInput validation schema
outputSchemaZod schemaNoOutput schema for structured responses
destructivebooleanNoSet true for tools that create, update, or delete data
authProviderstringNoAuth provider ID needed for this tool
serviceAccountAllowedbooleanNoWhether service accounts can use this tool
handlerfunctionYes(ctx: PluginContext, input: T) => Promise<U>

Handler context

The handler receives a PluginContext with five properties:
PropertyTypeDescription
ctx.userIdstringAuthenticated user’s ID
ctx.roleRoleUser’s role ({ id, name, patterns })
ctx.authAuthResolvergetToken(provider) returns a Bearer token
ctx.elicitElicitFnRequest user approval for destructive operations
ctx.loggerLoggerinfo(), warn(), error() for structured logging

Requesting approval

For destructive tools, call ctx.elicit() before performing the action:
async handler(ctx, input) {
  const approval = await ctx.elicit({
    toolPath: "my_service.items.delete",
    message: `Delete item "${input.id}"?`,
    args: input,
    type: "approval",
  });

  if (!approval.approved) {
    throw new Error("Deletion denied by user");
  }

  // proceed with deletion...
}
ctx.elicit() takes a single object with:
FieldTypeRequiredDefaultDescription
toolPathstringYesTool requesting approval
messagestringYesHuman-readable description
argsobjectNoThe arguments being approved
typestringNo"approval""approval" or "form"
Returns { executionId, approved, data? }.

Registering your plugin

Register the plugin as a source via the server API:
curl -X POST http://localhost:3000/api/registry/sources \
  -H "Authorization: Bearer $TOOLSHED_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "plugin",
    "id": "my-service",
    "namespace": "my_service",
    "pluginId": "my-service"
  }'