An MCP tool is not merely a function with a description. It is a capability contract crossing four trust boundaries:

text
user intent -> model selection -> MCP request -> business effect

Each boundary can fail independently. A model may select the wrong tool, produce schema-valid but semantically wrong arguments, call an operation the principal cannot perform, or misread an otherwise correct result. Production design must make each failure observable and containable.

This guide uses the 2026-07-28 MCP protocol model. If your client or server still negotiates an older revision, keep a compatibility matrix and test that profile separately rather than mixing assumptions.

Key Takeaways

  • Design four contracts: selection, input, output, and effect.
  • Expose business intent, not generic infrastructure primitives.
  • Keep identity, tenant, ownership, prices, and policy out of model arguments.
  • Use outputSchema with structuredContent for machine-consumable results.
  • Distinguish tool errors from protocol errors and input_required from completion.
  • Treat Tool Annotations as hints, never authorization.
  • Use opaque, integrity-protected handles for state that spans requests.
  • Bound result size, pagination, retries, wall time, and side effects.
  • Evaluate descriptions with real task distributions, including abstention cases.

The 2026-07-28 Protocol Context

The current protocol is stateless at the protocol-session layer:

  • clients call server/discover to learn supported versions and capabilities;
  • every request carries required protocol and client metadata in _meta;
  • the former initialize / initialized session handshake is not the current model;
  • result-bearing methods use resultType, normally complete or input_required;
  • multi-round requests carry explicit inputResponses and opaque requestState;
  • long-lived change delivery uses explicit subscriptions rather than an implicit session stream.

A maintained SDK should construct and validate protocol metadata. Tool handlers should focus on the business contract, but authors still need this model to avoid storing security state in a connection or assuming that one successful handshake authorizes later calls.

Four Contracts for Every Tool

1. Selection Contract

The name and description help a model decide whether to call the tool.

Prefer names that express one business action:

Weak Better Why
process invoice_list_overdue Names the resource and operation
get_data customer_get_profile Makes scope explicit
manage_order order_preview_refund Separates preview from commit
http_request Domain-specific read tools Avoids arbitrary network authority

A useful description states:

  1. what the tool does;
  2. when to use it;
  3. when not to use it;
  4. authoritative input meaning;
  5. output shape and limits;
  6. whether it reads, previews, or commits.

Do not put credentials, hidden policy, or long tutorials in the description. Descriptions are untrusted server-provided text and also consume model context.

2. Input Contract

Use the narrowest supported JSON Schema:

  • explicit required;
  • additionalProperties: false;
  • enums for stable business values;
  • bounded strings, arrays, and numbers;
  • separate fields with one meaning each;
  • cursor pagination instead of unbounded limits;
  • identifiers users actually know, not internal database keys.

Schema validation proves shape, not intent. A valid order_reference may belong to another tenant. A valid amount may be stale. A valid path may escape an allowed directory after symlink resolution.

Never accept these as model-controlled arguments:

text
principal_id, tenant_id, roles, is_admin, approved,
authoritative_price, credential, policy_decision

Derive them from authenticated server context and trusted systems of record.

3. Output Contract

MCP supports both:

  • content: human/model-consumable content blocks;
  • structuredContent: a machine-readable JSON value.

When a tool declares outputSchema, structuredContent must conform to it. Typed SDK helpers can infer and validate this contract. Keep important fields stable:

json
{
  "items": [
    {
      "invoice_reference": "INV-42",
      "currency": "USD",
      "amount_minor": 1299
    }
  ],
  "next_cursor": ""
}

Use integer minor units for money, explicit units and time zones, stable enums, and opaque cursors. Include source or freshness metadata when the caller needs to judge evidence.

Do not dump an entire database row, webpage, log stream, or exception into the result. Minimize fields, cap bytes and item counts, redact secrets and personal data, and return references for large artifacts.

4. Effect Contract

Define whether the tool is:

  • a pure read;
  • an externally observable read;
  • a preview;
  • an additive write;
  • an idempotent update;
  • a destructive or irreversible action.

For writes, document and enforce preconditions, idempotency key scope, conflict behavior, confirmation, and ambiguous-timeout recovery. A description that says “asks for confirmation” is not enforcement. Bind approval to a digest of canonical arguments and resource version in trusted code.

Annotations Are Hints

readOnlyHint, destructiveHint, idempotentHint, and openWorldHint can help a host label or rank tools. They are not verified claims. A malicious or buggy server can annotate a destructive tool as read-only.

The server must enforce actual behavior. The host should combine annotations with its own allowlist, trust policy, user consent, and observed tool identity.

Errors and Multi-Round Results

Keep three outcomes separate:

Outcome Representation Example
Protocol failure JSON-RPC / MCP error Unknown method, malformed protocol request
Tool execution failure Tool result with isError Invalid business state, downstream unavailable
More input required resultType: "input_required" User choice, credentials, confirmation

A recoverable tool error should expose a stable code, whether retry may help, and a safe next action. Do not expose stack traces, SQL, credentials, internal hostnames, or resource-existence details that violate authorization.

The 2026-07-28 multi-round trip result (MRTR) flow is:

text
tools/call
  -> resultType: input_required
     inputRequests: {server-assigned IDs ...}
     requestState: opaque state
  -> client fulfills requests
  -> tools/call with inputResponses + same requestState
  -> resultType: complete

Treat requestState as attacker-controlled on receipt. Authenticated deployments should bind it to the principal, tenant, operation, expiry, and original canonical arguments. Unauthenticated servers must encrypt, sign, and verify it. Never serialize raw credentials or unrestricted internal state into the handle.

Runnable Go SDK Example

The official Go SDK v1.8.0 supports the 2026-07-28 specification and requires Go 1.25 or newer. Its generic AddTool helper derives input and output schemas, validates input, populates structuredContent, and turns handler errors into tool errors.

This local stdio example scopes invoices to a trusted tenant captured by the server process. A remote server should obtain the same principal from authenticated request middleware, never from ListInvoicesInput.

go
package main

import (
	"context"
	"errors"
	"log"

	"github.com/modelcontextprotocol/go-sdk/mcp"
)

type ListInvoicesInput struct {
	Status string `json:"status" jsonschema:"invoice status: open or overdue"`
	Limit  int    `json:"limit" jsonschema:"maximum number of invoices to return"`
}

type Invoice struct {
	Reference   string `json:"invoice_reference"`
	Currency    string `json:"currency"`
	AmountMinor int64  `json:"amount_minor"`
}

type ListInvoicesOutput struct {
	Items      []Invoice `json:"items"`
	NextCursor string    `json:"next_cursor"`
}

type storedInvoice struct {
	TenantID string
	Status   string
	Invoice
}

type InvoiceService struct {
	tenantID string
	invoices []storedInvoice
}

func (s *InvoiceService) ListInvoices(
	ctx context.Context,
	request *mcp.CallToolRequest,
	input ListInvoicesInput,
) (*mcp.CallToolResult, ListInvoicesOutput, error) {
	if err := ctx.Err(); err != nil {
		return nil, ListInvoicesOutput{}, err
	}
	if input.Status != "open" && input.Status != "overdue" {
		return nil, ListInvoicesOutput{}, errors.New("invalid_status")
	}
	if input.Limit < 1 || input.Limit > 50 {
		return nil, ListInvoicesOutput{}, errors.New("invalid_limit")
	}

	output := ListInvoicesOutput{Items: []Invoice{}}
	for _, invoice := range s.invoices {
		if invoice.TenantID != s.tenantID || invoice.Status != input.Status {
			continue
		}
		output.Items = append(output.Items, invoice.Invoice)
		if len(output.Items) == input.Limit {
			break
		}
	}
	return nil, output, nil
}

func boolPointer(value bool) *bool {
	return &value
}

func main() {
	service := &InvoiceService{
		tenantID: "tenant-a",
		invoices: []storedInvoice{{
			TenantID: "tenant-a",
			Status:   "overdue",
			Invoice: Invoice{
				Reference:   "INV-42",
				Currency:    "USD",
				AmountMinor: 1299,
			},
		}},
	}

	server := mcp.NewServer(
		&mcp.Implementation{Name: "invoice-server", Version: "v1.0.0"},
		nil,
	)
	mcp.AddTool(server, &mcp.Tool{
		Name: "invoice_list",
		Description: "List open or overdue invoices visible to the authenticated " +
			"tenant. Use for invoice lookup, not for payment or reminders. " +
			"Returns at most 50 invoice summaries.",
		Annotations: &mcp.ToolAnnotations{
			ReadOnlyHint:  true,
			OpenWorldHint: boolPointer(false),
		},
	}, service.ListInvoices)

	if err := server.Run(
		context.Background(),
		&mcp.StdioTransport{},
	); err != nil {
		log.Fatal(err)
	}
}

The typed helper validates JSON shape, but the handler still enforces the status vocabulary and limit. More importantly, tenant identity is absent from model input. In a real repository query, include tenant scope in the database predicate and test that no cross-tenant reference changes the result.

State and Handles

Stateless protocol requests do not mean the business has no state. Store durable state in an authoritative system and return scoped handles:

text
handle -> principal + tenant + operation + resource version + expiry

Use a server-side record or an authenticated encrypted token. Handles should be opaque, unguessable, revocable when needed, and rejected when principal, purpose, version, or expiry no longer matches.

Do not rely on:

  • process-local maps in a horizontally scaled deployment;
  • an HTTP connection as user identity;
  • client-supplied session IDs as authorization;
  • replayable approval tokens with no operation digest;
  • model memory as workflow state.

Tool Discovery and Context Budget

There is no universal maximum tool count. Measure:

  • serialized schema tokens;
  • wrong-tool and no-tool rates;
  • argument accuracy;
  • first-token and task latency;
  • task success and recovery;
  • tool-specific policy violations.

Expose the smallest relevant set after deterministic routing and policy filtering. Discovery does not grant authority: an installed server or listed tool is not automatically trusted.

Testing Strategy

Contract Tests

  • minimal, typical, maximal, and malformed input;
  • unknown fields, enum variants, Unicode, and boundary values;
  • output conformance and stable error codes;
  • cancellation, timeout, duplicate request, and idempotency;
  • authorization with another tenant's identifier;
  • result-size and pagination limits.

Protocol Tests

  • server/discover and negotiated protocol version;
  • required per-request _meta;
  • tools list and call round trips;
  • resultType complete and input_required;
  • inputResponses / requestState integrity;
  • compatibility profiles supported by the deployment;
  • transport disconnect and retry behavior.

Use the official conformance suite and MCP Inspector where they fit, but keep business authorization tests in your own suite.

Model-in-the-Loop Tests

Create realistic utterances for:

  • positive selection;
  • choosing between similar tools;
  • abstaining when no tool applies;
  • asking for missing information;
  • preserving IDs, dates, currencies, and units;
  • malicious text in tool results;
  • claims of success after a tool error.

Run the same corpus when descriptions, schemas, model versions, routing, or tool catalogs change. Report confidence intervals and slice results by task type rather than publishing one aggregate score.

Production Checklist

  • [ ] One narrow business action per tool.
  • [ ] Description states use, non-use, output, and side-effect boundaries.
  • [ ] Input and output schemas are versioned and reviewed.
  • [ ] Trusted identity and policy are absent from model arguments.
  • [ ] Object-level authorization occurs before data or effects are returned.
  • [ ] Results are bounded, redacted, and provenance-aware.
  • [ ] Errors, completion, and input-required states are distinct.
  • [ ] Write operations have confirmation and idempotency semantics.
  • [ ] State handles are opaque, scoped, integrity-protected, and expiring.
  • [ ] Annotations match behavior but are never relied on as enforcement.
  • [ ] Deterministic, protocol, model, and adversarial tests run in CI.
  • [ ] Traces record versions and decisions without secrets.

Conclusion

Good MCP Tool design reduces ambiguity for the model while preserving deterministic authority in the server. The name and description guide selection, schemas define machine contracts, structured results support composition, and explicit result types make recovery possible. None of those replace authentication, object authorization, idempotency, or audit.

For the host-side execution loop, read the LLM tool-calling architecture guide. For transport behavior, continue with the Go MCP Streamable HTTP guide.

Primary Sources