Skip to main content
All posts

Designing Claude Connector Tools: Schemas, Descriptions, and Patterns for Reliable Tool Calls (August 2026)

Abe Wheeler
Claude ConnectorsClaude Connector FrameworkClaude Connector TestingClaude AppsMCP AppsMCP App FrameworkMCP ToolsTool Design
Designing tools that Claude calls correctly.

Designing tools that Claude calls correctly.

TL;DR: Claude chooses connector tools from the contract published by tools/list: name, title, description, input schema, output schema, annotations, and optional MCP App metadata. Give each tool one clear intent and one safety profile. Keep names under Claude’s 64-character limit, describe every input, use an outputSchema for typed results, return actionable execution errors, and carry workflow state through explicit handles. Test the published metadata, handler behavior, UI result, model selection, and real Claude connection as separate layers.


A Claude Connector can authenticate perfectly and still feel unreliable. Claude may call list-tickets when the user asked for one ticket, invent an assignee because the schema requires it, or skip a tool because two descriptions sound the same.

Those failures usually start in the tool contract. Claude does not read your handler code. It sees the metadata and schemas your MCP server publishes, then decides whether a tool fits and whether it can produce valid arguments.

This guide shows how to design that contract for current Claude review rules, the MCP 2026-07-28 tool model, and MCP Apps that add interactive UI to tool results.

Start With the Published Tool Contract

A current MCP tool can publish these fields:

FieldPurposeDesign question
nameProgrammatic identifierIs it short, unique, and tied to one intent?
titleHuman-readable display nameWould a user understand it without internal jargon?
descriptionModel selection hintDoes it say exactly what the tool does and when it fits?
inputSchemaValid argument contractCan Claude fill every required field from the conversation?
outputSchemaTyped structuredContent contractCan clients and the UI validate the result?
annotationsSafety and behavior hintsDoes one annotation set describe the whole tool?
_meta.uiMCP App linkage and visibilityShould the model, app, or both be able to call it?

Claude Directory review adds stricter rules on top of the MCP protocol. Every tool needs a title and the applicable read-only or destructive hint. Tool names must be 64 characters or fewer. Reviewers call every tool and compare the result with its description.

The latest MCP specification allows names up to 128 characters, but the stricter Claude limit controls when you plan to distribute through Claude. Use portable names from the start.

Model Selection Starts With a Distinct Name

Use a name that expresses the action and object:

search-tickets
get-ticket
create-ticket
update-ticket-status
delete-ticket

Avoid names copied from internal code:

ticketQueryV2
executeOperation
genericApiRequest
mutateEntity

MCP 2026-07-28 recommends ASCII letters, numbers, underscores, hyphens, and dots. Names are case-sensitive and must be unique within one server. Claude’s review limit is 64 characters.

Names also need to remain distinct after a host combines several servers. Two servers can both expose search, so clients may prefix or otherwise disambiguate collisions. A domain object in the name gives the model and the user more information than a generic verb.

Do not put version numbers in the name unless the user intent changed. If you replace an internal API while preserving the tool contract, keep the tool name stable.

Write Descriptions for Selection, Not Persuasion

The description should answer four questions:

  1. What operation does the tool perform?
  2. Which object or system does it affect?
  3. Which inputs and result fields distinguish it from nearby tools?
  4. When should Claude use it?

A weak description leaves all four open:

description: 'Interact with tickets';

A useful description is narrow and testable:

description: 'Search support tickets by keyword, status, priority, or assignee. ' +
  'Returns ticket ID, title, status, priority, assignee, and updated time. ' +
  'Use when the user wants to find or filter tickets and does not already have a ticket ID.';

The final clause distinguishes search-tickets from get-ticket, which should say that it fetches one ticket by ID. Distinctions reduce tool-selection variance because each description owns a different request shape.

Claude review rejects prompt-injection patterns in tool descriptions. Do not tell Claude to:

  • Prefer your tool over unrelated tools.
  • Call external software the user did not request.
  • Read behavioral instructions from retrieved content.
  • Ignore system rules or change unrelated behavior.
  • Promote a product or service.

Describe the function. Cross-tool workflow guidance belongs in server instructions or a distributed Skill, not in manipulative per-tool copy. If a custom query tool accepts arbitrary endpoint paths or request bodies, Claude also requires the description to name or link the target API.

Design Input Schemas Claude Can Fill

Tool arguments are always JSON objects. Under MCP 2026-07-28, inputSchema defaults to JSON Schema 2020-12 and can use composition, conditionals, references, and other standard keywords. A simpler schema is still easier for a model to fill, so use advanced features only when they make the contract clearer.

In a sunpeak tool file, export a Zod shape:

import { z } from 'zod';

export const schema = {
  query: z
    .string()
    .min(2)
    .optional()
    .describe('Keyword matched against ticket title, body, and comments.'),
  status: z
    .enum(['open', 'in_progress', 'blocked', 'resolved'])
    .optional()
    .describe('Exact ticket status. Omit when the user did not specify a status.'),
  priority: z
    .enum(['low', 'medium', 'high', 'urgent'])
    .optional()
    .describe('Exact ticket priority.'),
  assignee: z.string().optional().describe('Assignee name or email. Partial names are accepted.'),
  limit: z
    .number()
    .int()
    .min(1)
    .max(25)
    .optional()
    .describe('Maximum results to return. Defaults to 10.'),
  cursor: z.string().optional().describe('Cursor returned by the previous search call.'),
};

Each description resolves an ambiguity that a type cannot. A plain string does not say whether partial names work, and an optional number does not say its default.

Use these schema rules:

  • Require identifiers only when the operation cannot run without them.
  • Make search filters optional unless every valid search needs that field.
  • Use enums when the backend accepts a fixed set.
  • Add real minimums, maximums, formats, and string patterns.
  • Reject unknown fields when they could change behavior.
  • Keep dates explicit, such as an ISO 8601 timestamp with timezone.
  • Avoid freeform nested objects that Claude has to invent.

If the user rarely supplies a required value, split the workflow. Search for the object first, then pass its ID to a focused detail or write tool. Required fields should come from the user’s request, prior tool results, or an explicit clarification, not a guess.

For a tool with no parameters, publish an object schema that rejects unexpected keys:

{
  "type": "object",
  "additionalProperties": false
}

Split Tools at Safety and Intent Boundaries

A catch-all tool with an operation or HTTP method parameter makes selection and review harder:

export const schema = {
  operation: z.enum(['search', 'create', 'update', 'delete']),
  payload: z.record(z.unknown()),
};

This one schema contains unrelated required fields and several safety levels. Search is read-only, create is additive, and delete is destructive. One annotation set cannot describe all of them.

Claude review rejects a tool that combines safe HTTP methods such as GET with unsafe methods such as POST, PUT, PATCH, or DELETE. Split read and write tools, then split broad writes by action when their inputs or confirmation needs differ.

Do not overcorrect by creating dozens of near-duplicates. Keep one tool when the operation, safety profile, and result contract are genuinely the same. For example, one search-tickets tool can support several optional filters because they all express the same read intent.

A useful test is simple: can you describe the tool with one verb, one object, one annotation set, and one output shape? If not, split it.

Apply Annotations With Their Exact Meaning

MCP annotations are untrusted hints. They help a host decide how to present and confirm a call, but they never replace server-side authorization.

AnnotationMeaningMCP default when omitted
readOnlyHintThe tool does not modify its environmentfalse
destructiveHintA write may perform destructive updatestrue when not read-only
idempotentHintRepeating the same write has no additional effectfalse
openWorldHintThe tool may interact with external entitiestrue

For Claude Directory review, set the applicable values explicitly. A search against an external ticket service might use:

annotations: {
  readOnlyHint: true,
  destructiveHint: false,
  openWorldHint: true,
}

A status update might use:

annotations: {
  readOnlyHint: false,
  destructiveHint: true,
  idempotentHint: true,
  openWorldHint: true,
}

idempotentHint: true means repeating the same call with the same arguments has no additional effect. Setting a ticket status to resolved can be idempotent if the server does not send a new notification or append a new history record on every retry. Sending a message is usually not idempotent unless you enforce a caller-supplied idempotency key.

openWorldHint is about the interaction domain, not network topology. A web search is open world. A tool that reads one authenticated workspace may be closed world even if it calls a remote database. Document the actual behavior and test it.

Add an Output Schema for Typed Results

An input schema helps Claude call a tool. An outputSchema gives the result an equally clear contract.

sunpeak tool files can export a Zod output shape:

export const outputSchema = {
  tickets: z.array(
    z.object({
      id: z.string(),
      title: z.string(),
      status: z.enum(['open', 'in_progress', 'blocked', 'resolved']),
      priority: z.enum(['low', 'medium', 'high', 'urgent']),
      assignee: z.string().nullable(),
      updatedAt: z.string(),
    })
  ),
  total: z.number().int().nonnegative(),
  nextCursor: z.string().nullable(),
};

The handler returns matching structuredContent:

export default async function searchTickets(args: Args) {
  const page = await ticketClient.search(args);
  const structuredContent = {
    tickets: page.items.map(toTicketSummary),
    total: page.total,
    nextCursor: page.nextCursor ?? null,
  };

  return {
    structuredContent,
    content: [
      {
        type: 'text' as const,
        text: JSON.stringify(structuredContent),
      },
    ],
  };
}

MCP 2026-07-28 lets outputSchema describe any JSON value, including arrays and primitives. When an output schema exists, the server must return conforming structuredContent, and clients should validate it. MCP recommends a serialized JSON text block as well for backward compatibility.

For large results, a compact model summary may be more practical than duplicating the whole payload. Check the hosts you support, document that compatibility choice, and keep the UI contract in structuredContent. Claude.ai and Desktop currently cap tool results at about 150,000 characters, while Claude Code uses a configurable token limit, so pagination is safer than relying on the ceiling.

Design Result Channels Deliberately

Tool results can carry several kinds of data:

ChannelTypical consumerPut this here
contentModel and conversationText summary, images, resource links, or embedded resources
structuredContentModel, host, typed client, and MCP AppModel-safe typed data that matches outputSchema
_metaHost or app-specific plumbingData that the model does not need, when the host supports it
isErrorModel and clientWhether a valid tool call failed during execution

Do not put secrets in any result channel. Treat the model-visible contract as user data because hosts may include it in context, logs, or traces.

For list tools, return compact records and pagination data. Use a separate get-by-ID tool for full detail. Stable IDs let later tools refer to an object without repeating all of its fields.

The result should answer the next likely question. A search item needs enough fields for the user and model to choose one record. The detail tool can return comments, history, and long descriptions after that choice.

An MCP App combines a callable tool with a ui:// resource. The current MCP Apps metadata format links them through _meta.ui.resourceUri; the older flat ui/resourceUri key remains a compatibility fallback.

sunpeak lets a tool reference a resource directory by name and emits the app metadata:

import type { AppToolConfig } from 'sunpeak/mcp';

export const tool: AppToolConfig = {
  resource: 'ticket-list',
  title: 'Search Tickets',
  description:
    'Search support tickets by keyword, status, priority, or assignee. Returns compact ticket summaries.',
  annotations: {
    readOnlyHint: true,
    destructiveHint: false,
    openWorldHint: true,
  },
  _meta: {
    ui: { visibility: ['model', 'app'] },
  },
};

The model calls the tool, the host reads the linked resource, and the View renders structuredContent. Keep the text result useful for hosts that do not render MCP Apps.

Tool visibility is separate from safety annotations. An app-only helper can still be destructive. A model-visible UI tool can still be read-only.

For a confirmed write, the View can call an app-only helper:

export const tool: AppToolConfig = {
  title: 'Apply Ticket Status Update',
  description: 'Apply one reviewed status update to the specified support ticket.',
  annotations: {
    readOnlyHint: false,
    destructiveHint: true,
    idempotentHint: true,
    openWorldHint: true,
  },
  _meta: {
    ui: { visibility: ['app'] },
  },
};

The server must still verify the user’s identity, workspace, permission, ticket ID, allowed transition, and confirmation context. Hiding a tool from the model does not make it authorized.

Carry Workflow State With Explicit Handles

MCP 2026-07-28 removed protocol-level sessions. Any request can reach any server instance, so hidden connection state cannot reliably connect one tool call to the next.

Use explicit opaque handles:

create-ticket-draft -> returns draftId
update-ticket-draft -> requires draftId
publish-ticket-draft -> requires draftId

The creation tool should state how long the handle remains valid. Every later tool must authorize the current caller against the referenced object. A handle is a name, not proof of permission.

Return a tool execution error when the handle is expired or unknown:

return {
  isError: true,
  content: [
    {
      type: 'text' as const,
      text: 'Draft draft_123 expired. Create a new ticket draft and retry the update.',
    },
  ],
};

That message tells Claude how to recover. A generic Internal Server Error does not.

Separate Protocol Errors From Execution Errors

MCP has two error levels.

Protocol errors cover malformed JSON-RPC, unknown tool names, and invalid request structure. The client or integration code usually needs to fix these.

Tool execution errors use a normal tool result with isError: true. They cover invalid business input, denied access, expired handles, upstream failures, and conflicts that Claude may be able to correct.

Write execution errors with:

  • The field or resource that failed.
  • The constraint or permission that blocked it.
  • Whether retrying is safe.
  • The next valid action.

Do not leak stack traces, database details, tokens, or internal hostnames. Log the technical cause on the server with a correlation ID, then return a short user-safe message.

Claude review calls every tool with valid parameters and expects a successful result. It also checks that invalid data produces an actionable error instead of a silent fallback or generic response.

Keep the Tool Catalog Stable and Scoped

MCP 2026-07-28 says tools/list should return tools in a deterministic order when the underlying set has not changed. Stable ordering improves client caching and model prompt-cache hits.

The visible set may vary by the authorization on each request. A token with read-only scopes can receive fewer tools than an administrator token. The set should not change because another request happened on the same connection.

That affects tool design in three ways:

  1. Register tools in a stable order or sort the final catalog.
  2. Derive visibility from explicit per-request authorization, not mutable connection state.
  3. Keep descriptions stable unless the behavior changed, because metadata churn invalidates caches and can change model selection.

A smaller, distinct catalog usually routes better than a large set of overlapping tools. Remove aliases after clients migrate, and avoid exposing internal maintenance tools to the model.

Test Metadata as a Product Contract

A handler unit test does not prove the published tool is usable. Inspect tools/list and call the server through MCP:

import { expect, test } from 'sunpeak/test';

test('publishes the search-tickets contract', async ({ mcp }) => {
  const tools = await mcp.listTools();
  const search = tools.find((candidate) => candidate.name === 'search-tickets');

  if (!search) throw new Error('search-tickets was not published');

  expect(search).toMatchObject({
    title: 'Search Tickets',
    annotations: {
      readOnlyHint: true,
      destructiveHint: false,
      openWorldHint: true,
    },
  });
  expect(search.name.length).toBeLessThanOrEqual(64);
  expect(search.inputSchema).toMatchObject({
    properties: { query: expect.any(Object) },
  });
  expect(search.outputSchema).toBeDefined();
});

test('returns a result that matches the output contract', async ({ mcp }) => {
  const result = await mcp.callTool('search-tickets', {
    priority: 'high',
    limit: 10,
  });

  expect(result.isError).toBeFalsy();
  expect(result.structuredContent).toMatchObject({
    tickets: expect.any(Array),
    total: expect.any(Number),
  });
});

Add automated checks for every tool:

  • Name length and allowed characters.
  • Unique names and titles.
  • A non-empty, narrow description.
  • Descriptions on every input field.
  • Accurate read-only and destructive hints.
  • An output schema that validates every success result.
  • No model-visible app-only helper tools.
  • No prompt-injection phrases or secrets.
  • Deterministic catalog order for the same authorization.

Then test behavior with valid, invalid, empty, forbidden, timeout, conflict, and upstream-failure inputs. For MCP Apps, render success, empty, loading, error, and confirmation states against fixed simulations.

Measure Tool Selection With Evals

Metadata can be valid and still route poorly. Write eval cases around real user language, especially where two tools are close:

  • “Find open tickets assigned to Sarah” should call search-tickets.
  • “Open TICK-1234” should call get-ticket.
  • “Move TICK-1234 to resolved” should call the review or update flow.
  • “What can I do with tickets?” may need no tool call.

Run each case several times and use partial argument matching for values with more than one valid form. Track the model identifier, tool catalog hash, prompt, selected tool, arguments, and pass rate. A single passing run does not measure selection reliability.

When an eval fails, change one signal at a time. Rename the tool, narrow the description, improve a field description, or remove an overlapping alias, then rerun the same case set. That tells you which metadata change improved routing.

Finish With Real Claude Testing

Claude asks connector developers to exercise every tool with MCP Inspector and as a custom connector before Directory submission. A real-host test catches behavior that schema tests cannot:

  • The deployed HTTPS server is reachable and authenticates correctly.
  • Claude sees the expected tools for the test account’s scopes.
  • Real prompts select the intended tool.
  • Write calls show the expected confirmation behavior.
  • MCP App resources load and render in the host.
  • Result sizes and timeouts stay within host limits.

Use a fully populated account so searches return data and write tools have safe records to modify. Record the prompt, tool name, arguments, result, server commit, and correlation ID for each review case.

Live tests should remain a small final layer. Run deterministic metadata, protocol, handler, and UI tests on every change, then use real Claude for the few host behaviors you cannot reproduce locally.

Tool Design Checklist

Before shipping a Claude Connector tool, verify:

  1. The name is unique, portable, and 64 characters or fewer.
  2. The title is clear to a user.
  3. The description states the exact action, object, result, and selection boundary.
  4. The description contains no prompt-injection or promotional instructions.
  5. Every required input is available from the user or a prior result.
  6. Every field has a description and real constraints.
  7. Read and write actions use separate tools.
  8. Annotations match the entire tool’s behavior.
  9. outputSchema matches every successful structuredContent value.
  10. List results are compact and paginated.
  11. MCP App tools link the right ui:// resource and use correct visibility.
  12. Multi-step state uses an explicit authorized handle.
  13. Execution errors explain how to recover without leaking internals.
  14. tools/list is deterministic for the same authorization.
  15. Protocol tests, UI simulations, evals, and real-host checks cover the tool.

Where sunpeak Helps

sunpeak auto-discovers tool files, converts Zod input and output shapes, links tools to MCP App resources, and exposes the published server through protocol and Playwright fixtures. Its inspector replicates supported host runtimes locally, so you can test tool metadata, typed results, annotations, visibility, UI states, and app-initiated calls without refreshing a paid host account after every code change.

You can scaffold tests for a sunpeak project or any MCP server:

npx sunpeak test init --server https://example.com/mcp

Clear tool design comes first. The framework then makes that contract cheap to inspect and hard to regress as the connector grows.

Get Started

Documentation →
npx sunpeak new

Further Reading

Frequently Asked Questions

How does Claude decide which connector tool to call?

Claude compares the user request and conversation context with the enabled tool catalog. The strongest signals are the tool name, human-readable title, narrow description, input schema, and the descriptions and constraints on each input field. Claude also considers whether required arguments are available and whether annotations mark the tool as read-only or state-changing. Tool metadata should describe the tool contract, not instruct Claude to prefer your product or ignore other tools.

What makes a good Claude Connector tool description?

Start with the action and domain object, state the exact operation, list the important filters or identifiers, describe the compact result, and explain the trigger only when the name is not enough. Keep overlapping tools distinct. A good search description says which fields can be searched and which fields come back; a write description names the exact side effect. Claude review rejects descriptions that contain prompt-injection patterns or behavior unrelated to the tool.

How long can a Claude Connector tool name be?

Claude Connector Directory review limits tool names to 64 characters, even though MCP 2026-07-28 recommends a broader 1-to-128-character range. Use short ASCII names made from letters, numbers, hyphens, underscores, or dots. Keep names unique within the server and prefer intent-based names such as search-tickets or update-ticket-status over internal API names.

Should I use one large tool or several focused Claude Connector tools?

Split tools when operations have different user intents, input shapes, or safety annotations. Read and write operations must be separate for Claude review, and broad write tools are easier to confirm when split into create, update, and delete actions. Do not split one coherent operation into many near-duplicates, because overlapping descriptions and a large tool catalog can make selection harder. Each tool should have one annotation set and one clear reason to call it.

Which annotations are required for Claude Connector tools?

Every submitted tool needs a title and its applicable safety hint. Use readOnlyHint: true only when the tool never changes state. Claude requires destructiveHint: true for tools that modify or delete data. idempotentHint is useful when repeating a write with the same arguments has no additional effect, and openWorldHint indicates that the tool can interact with external entities. These are hints, so the server must still authenticate, authorize, validate, and confirm sensitive actions.

What are inputSchema, outputSchema, and structuredContent in an MCP tool?

inputSchema is a JSON Schema object for tool arguments. outputSchema is an optional JSON Schema for structuredContent. Under MCP 2026-07-28, schemas default to JSON Schema 2020-12, input arguments must have an object root, and outputSchema may describe any JSON value. If a tool declares outputSchema, its structuredContent must match. Text content remains useful for model-readable summaries and backward compatibility, while MCP Apps render structuredContent.

How should a multi-step Claude Connector workflow store state?

Return an explicit opaque handle from the tool that creates the workflow state, then require that handle in later calls. MCP 2026-07-28 has no protocol-level session, so a server cannot rely on hidden connection state to connect one call to the next. Authorize the caller against the handle on every request, document its lifetime, and return an actionable execution error when it expires.

How do I test whether Claude calls my connector tools correctly?

Inspect tools/list to verify names, titles, descriptions, schemas, output schemas, annotations, UI metadata, and deterministic ordering. Call every tool with valid, invalid, empty, unauthorized, and upstream-failure inputs. Render MCP App states with fixed simulation data, run model evals for ambiguous selection cases, and finish with a custom-connector smoke test in Claude. Claude review asks developers to exercise every tool with MCP Inspector and as a custom connector.