> ## Documentation Index
> Fetch the complete documentation index at: https://docs.toolshed.philo.ventures/llms.txt
> Use this file to discover all available pages before exploring further.

# Writing Plugins

> Build a Toolshed plugin with definePlugin and defineTool.

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`:

```typescript theme={null}
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`:

| Field                   | Type       | Required | Description                                                                                 |
| ----------------------- | ---------- | -------- | ------------------------------------------------------------------------------------------- |
| `path`                  | `string`   | Yes      | Dot-separated path (e.g., `github.issues.create`). See [Tool Paths](/reference/tool-paths). |
| `name`                  | `string`   | Yes      | Title Case display name (e.g., "Create Issue")                                              |
| `description`           | `string`   | Yes      | What the tool does                                                                          |
| `inputSchema`           | Zod schema | Yes      | Input validation schema                                                                     |
| `outputSchema`          | Zod schema | No       | Output schema for structured responses                                                      |
| `destructive`           | `boolean`  | No       | Set `true` for tools that create, update, or delete data                                    |
| `authProvider`          | `string`   | No       | Auth provider ID needed for this tool                                                       |
| `serviceAccountAllowed` | `boolean`  | No       | Whether service accounts can use this tool                                                  |
| `handler`               | function   | Yes      | `(ctx: PluginContext, input: T) => Promise<U>`                                              |

## Handler context

The handler receives a [`PluginContext`](/reference/plugin-context) with five properties:

| Property     | Type           | Description                                          |
| ------------ | -------------- | ---------------------------------------------------- |
| `ctx.userId` | `string`       | Authenticated user's ID                              |
| `ctx.role`   | `Role`         | User's role (`{ id, name, patterns }`)               |
| `ctx.auth`   | `AuthResolver` | `getToken(provider)` returns a Bearer token          |
| `ctx.elicit` | `ElicitFn`     | Request user approval for destructive operations     |
| `ctx.logger` | `Logger`       | `info()`, `warn()`, `error()` for structured logging |

## Requesting approval

For destructive tools, call `ctx.elicit()` before performing the action:

```typescript theme={null}
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:

| Field      | Type     | Required | Default      | Description                  |
| ---------- | -------- | -------- | ------------ | ---------------------------- |
| `toolPath` | `string` | Yes      | --           | Tool requesting approval     |
| `message`  | `string` | Yes      | --           | Human-readable description   |
| `args`     | `object` | No       | --           | The arguments being approved |
| `type`     | `string` | No       | `"approval"` | `"approval"` or `"form"`     |

Returns `{ executionId, approved, data? }`.

## Registering your plugin

Register the plugin as a source via the server API:

```bash theme={null}
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"
  }'
```
