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

# Tool sets

> Point a tool set at an MCP server, an API, or your own code. Store secrets, curate what syncs, and hand it to an agent.

A [tool set](/docs/guides/tool-sets) is how an agent does more than talk. You point it at a provider, Cadenya discovers the tools behind it, and any [variation](/docs/guides/sdk/agents) you assign it to can call them. This page covers the four adapter kinds, secrets, curation, and wiring a set to an agent.

## What you need

* Your API key in `CADENYA_API_KEY` and a workspace ID in `CADENYA_WORKSPACE_ID`.

## Four adapters, one shape

Every tool set has one `spec.adapter`, and its `type` field names where the tools come from, with the matching key alongside it:

| Adapter   | Points at                                   | Tools come from                     |
| --------- | ------------------------------------------- | ----------------------------------- |
| `mcp`     | A Model Context Protocol server             | The server's tool list              |
| `openapi` | An OpenAPI spec, fetched by URL or uploaded | One tool per operation              |
| `http`    | A REST API base URL                         | Tools you define by hand            |
| `bare`    | Nothing                                     | Tools you define, calls you fulfill |

## The quick path: MCP

An MCP server is the fastest way to a working tool set. Point at the URL and the tools sync in the background, then refresh hourly.

```typescript theme={null}
import Cadenya from '@cadenya/cadenya';

const client = new Cadenya({ apiKey: process.env.CADENYA_API_KEY });
const workspaceId = process.env.CADENYA_WORKSPACE_ID!;

const toolSet = await client.toolSets.create({
  workspaceId,
  metadata: { name: 'Faker', externalId: 'faker' },
  spec: { adapter: { type: 'mcp', mcp: { url: 'https://free.cadenya.com/faker-mcp' } } },
});

console.log(toolSet.state); // STATE_ACTIVE
```

The call returns `STATE_ACTIVE` at once, but discovery happens in the background, so `info.toolCount` reads `0` for a moment. There is no sync endpoint. To block until the tools exist, poll the [event log](/docs/api-reference/toolservice/list-tool-set-events):

```typescript theme={null}
const events = await client.toolSets.listEvents(toolSet.metadata.id, { workspaceId });
for await (const event of events) {
  if (event.event?.type === 'syncCompleted') { console.log('synced', event.event.syncCompleted.toolsSynced); break; }
  if (event.event?.type === 'syncFailed') throw new Error(event.event.syncFailed.message);
}
```

A `syncCompleted` can still carry a `message` listing tools that failed while the sync as a whole succeeded, so compare `toolsSynced` against what you expect.

## OpenAPI: an API becomes a tool set

Point at a spec URL and each named operation becomes a tool. Filters can keep only the operations an agent needs.

```typescript theme={null}
await client.toolSets.create({
  workspaceId,
  metadata: { name: 'OpenAPI validator', externalId: 'openapi-validator' },
  spec: {
    adapter: {
      type: 'openapi',
      openapi: {
        type: 'url',
        url: 'https://validator.swagger.io/validator/openapi.json',
        baseUrl: 'https://validator.swagger.io/validator',
        includeTools: {
          operator: 'OPERATOR_AND',
          filters: [
            {
              attribute: 'ATTRIBUTE_NAME',
              matcher: {
                type: 'exact',
                exact: 'reviewByUrl',
                caseSensitive: false,
              },
            },
          ],
        },
      },
    },
  },
});
```

This configuration processes six operations and leaves `reviewByUrl` available while the other five remain omitted. A URL-backed OpenAPI set re-syncs hourly, so it tracks the upstream specification.

## Bare and HTTP: your code is the tool

A `bare` or `http` set starts empty; you define the tools. The difference is who runs them. An `http` tool calls the base URL for you. A `bare` tool fires nothing, your code fulfills the call.

```typescript theme={null}
const toolSet = await client.toolSets.create({
  workspaceId,
  metadata: { name: 'Orders', externalId: 'orders' },
  spec: { adapter: { type: 'bare', bare: {} } },
});

await client.toolSets.tools.create(toolSet.metadata.id, {
  workspaceId,
  metadata: { name: 'issue_refund' },
  spec: {
    description: 'Refund a customer order.',
    requiresApproval: true,
    parameters: {
      type: 'object',
      properties: { orderId: { type: 'string' }, amount: { type: 'number' } },
      required: ['orderId'],
    },
    config: { type: 'bare', bare: {} },
  },
});
```

All four spec fields are required and enforced: a misspelled `parameters` or a missing `config` is a `400`, not a silently broken tool. `llmToolName` comes back cleaned up: `issue_refund` becomes `IssueRefund`, the name the model calls.

When an agent calls a bare tool, the objective parks at `TOOL_CALL_EXECUTION_STATUS_WAITING_FOR_CONTENT`. Your worker finds it and answers:

```typescript theme={null}
const parked = await client.objectives.toolCalls.list(objectiveId, {
  workspaceId,
  executionStatus: 'TOOL_CALL_EXECUTION_STATUS_WAITING_FOR_CONTENT',
});

for await (const call of parked) {
  const result = await refundOrder(call.data.arguments); // your code, your infrastructure
  await client.objectives.toolCalls.setContent(
    objectiveId,
    call.metadata.id,
    {
      workspaceId,
      content: [{ type: 'text', text: { text: JSON.stringify(result) } }],
    },
  );
}
```

That is a reverse harness: Cadenya never needs a route into your network.

## Secrets: reference, never inline

A tool set that talks to a real API needs a credential. Store it as a secret and reference it by name; never put the value in an adapter header, which is returned on read.

```typescript theme={null}
// The adapter names a placeholder.
const toolSet = await client.toolSets.create({
  workspaceId,
  metadata: { name: 'Orders API', externalId: 'orders-api' },
  spec: { adapter: { type: 'http', http: { baseUrl: 'https://api.example.com', headers: { Authorization: 'Bearer ${ORDERS_TOKEN}' } } } },
});

// The value lives separately, and never comes back.
await client.toolSets.secrets.create(toolSet.metadata.id, {
  workspaceId,
  metadata: { name: 'ORDERS_TOKEN' },
  spec: { value: process.env.ORDERS_TOKEN! },
});
```

Cadenya swaps `${ORDERS_TOKEN}` in at call time, so the plaintext lives only in the request Cadenya makes. Read the secret back and its value is `""`, always. A per-run [objective secret](/docs/api-reference/objectiveservice/create-a-new-objective) of the same name overrides this one, which is how a per-user token beats a shared service credential.

## Curate what syncs

For a synced set, curation lives on the adapter, not on the tools, because a filter is reapplied on every sync while a per-tool change is not.

```typescript theme={null}
await client.toolSets.update(toolSetId, {
  workspaceId,
  spec: {
    adapter: {
      type: 'mcp',
      mcp: {
        url: 'https://free.cadenya.com/faker-mcp',
        excludeTools: {
          operator: 'OPERATOR_AND',
          filters: [{ attribute: 'ATTRIBUTE_NAME', matcher: { type: 'contains', contains: 'Curse', caseSensitive: false } }],
        },
      },
    },
  },
});
```

Editing the adapter also triggers a re-sync, which is the only way to force one. Use `includeTools` for an allowlist instead.

## Hand it to an agent

A tool set does nothing until a [variation](/docs/api-reference/agentvariationservice/add-an-assignment-to-a-variation) assigns it. Assign the whole set, and tools discovered on a later sync reach the agent automatically:

```typescript theme={null}
await client.agents.variations.addAssignment(agentId, variationId, {
  workspaceId,
  type: 'toolSetId',
  toolSetId: toolSet.metadata.id,
});
```

Publish the agent, and its next objective can call every tool in the set. To confirm what an agent got, read [the objective's tools](/docs/api-reference/objectiveservice/list-objective-tools), which is the resolved, deduplicated, filtered list, not the raw set.

## Retire a set

Archive stops the sync and hides the set but keeps it, and works even while a variation still assigns it. Delete is permanent and refuses while assigned.

```typescript theme={null}
await client.toolSets.archive(toolSetId, { workspaceId });   // reversible
await client.toolSets.unarchive(toolSetId, { workspaceId });
await client.toolSets.delete(toolSetId, { workspaceId });     // permanent, refused if assigned
```

## Next steps

<CardGroup cols={2}>
  <Card title="Connect an MCP server" icon="plug" href="/docs/guides/tool-sets/mcp">
    The hands-on MCP lesson, end to end.
  </Card>

  <Card title="Run tools in your code" icon="code" href="/docs/guides/tool-sets/bare">
    Define a Bare tool and fulfill its calls from your application.
  </Card>

  <Card title="Approve a tool call" icon="hand" href="/docs/guides/callbacks/approving-a-tool">
    Gate a `requiresApproval` tool behind a human.
  </Card>

  <Card title="Store and use secrets" icon="key" href="/docs/guides/store-and-use-secrets">
    The full precedence story, workspace to objective.
  </Card>

  <Card title="Preventing tool bloat" icon="filter" href="/docs/guides/preventing-tool-bloat">
    Progressive discovery, for a set too big to load at once.
  </Card>
</CardGroup>
