---
title: "MCP: what it is, how to build a server and which tools to choose"
source: "https://www.matteovincenti.com/en/blog/mcp-server-tools-skills-guide"
language: en
author: "Matteo Vincenti"
published: 2026-10-20
updated: 2026-10-24
category: "AI"
tags: ["MCP", "AI integrations", "Automation"]
---

# MCP: what it is, how to build a server and which tools to choose

What MCP does and when to adopt it: business processes, costs, permissions and decision criteria, with a complete server example.

![A shared green connector joins three differently shaped sockets](https://cdn.sanity.io/images/xrqkwry7/production/cdfe9369457403ad4b8bddcff2412a83f5fa84c9-1727x910.png?w=1400&fit=max&auto=format)

Category: AI · Tags: MCP, AI integrations, Automation

Preparing a proposal from a catalogue means finding the right records, reading their specifications and identifying missing information. If the AI assistant cannot consult the system, someone has to export the data, pass it to the chat and put the result back where it belongs.

MCP, the Model Context Protocol, gives an AI application an interface for consulting data and calling functions in other software. A server exposes recognisable operations, such as reading a record or saving a draft; the application can make them available to the model and manage their execution. [MCP architecture](https://modelcontextprotocol.io/docs/learn/architecture).

I described a product-data application in my Italian article [MCP-compatible SaaS: how On Page® integrates AI into its PIM](https://onpage.it/blog/saas-compatibile-con-mcp-come-on-page-integra-lai-nel-pim/). A PIM holds product information: connecting it to an assistant lets people work with those records under their existing permissions. Here I focus on what makes that connection usable: which operations to expose, how to test them and where to enforce controls.

## Three reading paths

If you need to **decide whether to adopt MCP**, start with how it works and the comparison with CLIs and APIs. The GitHub project selection then helps you assess an existing integration.

To **build your first server**, go to “Build a local MCP server”. It includes prerequisites, complete files and a client that checks nine cases. It uses demonstration data and needs no external account.

To **design or maintain an integration**, start with “Anatomy of an MCP tool”. Names, schemas, annotations, responses, authorisation and host differences are explained through the same catalogue operation. The final sections connect that contract to skills and token consumption.

## How MCP works

The host is the application you use to work with the model. Inside it, an MCP client communicates with a server that exposes accessible capabilities. The server can run on your computer or on remote infrastructure.

This separation makes an integration reusable across compatible hosts. By itself, it does not determine how the application selects tools or how much material enters the model's context: those are responsibilities of the host and its workflow. [MCP architecture](https://modelcontextprotocol.io/docs/learn/architecture).

**How MCP works**

1. Host: AI application
2. MCP client
3. MCP server
4. Data or service

_Skill: work procedure → Host: AI application_

In the diagram, the protocol concerns the client–server connection. The skill guides the work performed by the application. The final service can retain its own APIs: MCP can provide an access layer to those APIs.

### Tools, resources and prompts

A tool is a callable function, such as retrieving a project's issue summary. A resource exposes content, such as a document or schema. A prompt provides a reusable interaction template.

These are distinct capabilities. For an initial integration, choose the one that represents the need rather than turning every piece of information into a generic command. The official architecture documentation distinguishes these primitives. [MCP primitives](https://modelcontextprotocol.io/docs/learn/architecture).

For the local test, we will use an issue summary: the tool will take a project identifier and return a small, known dataset. It is a smaller example than the catalogue, chosen to test calls and errors without connecting external services or accounts. The anatomy section will return to product records and the different consequences of reading, saving and sending.

## When I would choose MCP, a CLI or a direct API

If an activity lives entirely in a local script, a CLI may already provide the necessary interface. Reading Git status does not automatically justify adding a server: there needs to be a concrete reason, such as reuse across hosts, centralised controls or a capability the client lacks.

A direct API is a good candidate when you control the application and the call is simple. You can validate inputs and outputs without introducing an MCP server to manage.

MCP becomes interesting when you want to expose recognisable capabilities to several compatible applications, or connect an agent to an external system through an explicit contract. You still need to maintain the integration, manage versions and verify client behaviour.

_When I would choose MCP, a CLI or a direct API_

| Need | First candidate to assess | Decisive question |
| --- | --- | --- |
| Deterministic local operation | CLI or script | Is an additional interface actually needed? |
| A few calls inside my application | Direct API or SDK | Who will use the contract beyond this application? |
| A capability reusable across AI hosts | MCP server | Do the required clients support the necessary features? |
| A recurring procedure with several steps | Skill | Which tools must it coordinate? |

A recurring procedure can combine these options: a skill describes the work and uses the available tools, including a CLI or MCP. The interface decision concerns who needs to call the operation and which contract you are prepared to maintain.

## Build a local MCP server: the complete example

We build `get_project_summary`: it takes a project name and returns its open and closed issue counts. The data is a fixture, a small dataset written into the code: for `demo`, we expect three open issues and seven closed ones. That lets us check results and errors without connecting a GitHub account.

You need a terminal, Node.js with npm and a text editor. The test on 24 October 2026 uses Node.js 22.23.4 and npm 10.9.9. First, check that both commands are available:

```bash
node --version
npm --version
```

The example uses JavaScript ESM, `@modelcontextprotocol/server@2.3.1`, `@modelcontextprotocol/client@2.3.1` and `zod@4.6.7`. In SDK v2, the client and server are separate packages. Zod expresses the schemas the SDK exposes to the client; the SDK also supports other Standard Schema-compatible libraries. [SDK v2 packages](https://ts.sdk.modelcontextprotocol.io/v2/get-started/packages.html), [tools and schemas](https://ts.sdk.modelcontextprotocol.io/v2/servers/tools.html).

The reproducible material is available here, before the steps:

Download: [MCP v2: source files, pinned dependencies and nine local checks](https://cdn.sanity.io/files/xrqkwry7/production/0ef9ba22c144dc9e0fd1743616a3f934d2eafabd.json)

The JSON’s `files` property contains the text of four files: `server.mjs`, `client.mjs`, `package.json` and `package-lock.json`. You can follow the from-scratch setup below, or copy each text value into its corresponding file in an empty directory and install the pinned tree with `npm ci`. Either route produces this directory:

```txt
mcp-project-demo/
  package.json
  package-lock.json
  server.mjs
  client.mjs
```

In the from-scratch route, npm generates the first two files; you create the other two using the contents in the following sections. No project-type change is needed: the `.mjs` extension enables ESM imports.

```bash
mkdir mcp-project-demo
cd mcp-project-demo
npm init -y
npm install --save-exact @modelcontextprotocol/server@2.3.1 @modelcontextprotocol/client@2.3.1 zod@4.6.7
```

### The server

Save this content in `server.mjs`:

```javascript
import { McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import { z } from 'zod/v4';

const projects = { demo: { open: 3, closed: 7, source: 'fixture-v1' } };

serveStdio(() => {
  const server = new McpServer({ name: 'project-summary', version: '2.0.0' });
  server.registerTool('get_project_summary', {
    description: 'Read issue counts from the local demo fixture. No live data.',
    inputSchema: z.object({ project: z.string().min(1).max(40) }),
    outputSchema: z.object({
      project: z.string(), open: z.number(), closed: z.number(), source: z.string()
    }),
    annotations: { readOnlyHint: true, openWorldHint: false }
  }, async ({ project }) => {
    if (!Object.hasOwn(projects, project)) {
      return { isError: true, content: [{ type: 'text', text: 'Unknown project. Available: demo.' }] };
    }
    const result = { project, ...projects[project] };
    return { structuredContent: result, content: [{ type: 'text', text: JSON.stringify(result) }] };
  });
  return server;
});
```

The contract limits the identifier to a non-empty string of up to 40 characters. The result declares the project, counts and provenance. The `source` property lets the reader recognise that the data comes from a fixture.

The `Object.hasOwn` check prevents inherited JavaScript object properties from being treated as projects. This small choice makes the accepted identifiers explicit.

A successful response contains both structured data and a text representation for clients that use it. Later we will look at the protocol payload and how it differs from this handler’s return value. [SDK v2 results](https://ts.sdk.modelcontextprotocol.io/v2/servers/tools.html).

### Errors must remain errors

An unknown project returns `isError: true` and a useful message. It does not return zero counts: zero would mean the project exists and has no issues.

Invalid input must also fail before it is interpreted as a legitimate request. An empty string, a number and an identifier that is too long differ from a missing project, but none should produce an apparently valid summary.

This matters when a model reads the result: an ambiguous response can become the premise of a later action.

### The verification client

Save the second file as `client.mjs`:

```javascript
import assert from 'node:assert/strict';
import { fileURLToPath } from 'node:url';
import { Client } from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';

const client = new Client({ name: 'article-check', version: '2.0.0' }, {
  versionNegotiation: { mode: { pin: '2026-07-28' } }
});
const transport = new StdioClientTransport({
  command: process.execPath,
  args: [fileURLToPath(new URL('./server.mjs', import.meta.url))]
});
let checks = 0;
try {
  await client.connect(transport);
  const { tools } = await client.listTools();
  assert.equal(tools.length, 1); checks++;
  assert.equal(tools[0].name, 'get_project_summary'); checks++;
  const demo = await client.callTool({ name: 'get_project_summary', arguments: { project: 'demo' } });
  assert.deepEqual(demo.structuredContent, { project: 'demo', open: 3, closed: 7, source: 'fixture-v1' }); checks++;
  const missing = await client.callTool({ name: 'get_project_summary', arguments: { project: 'missing' } });
  assert.equal(missing.isError, true); checks++;
  for (const project of ['', 'x'.repeat(41), 12, '__proto__']) {
    const result = await client.callTool({ name: 'get_project_summary', arguments: { project } });
    assert.equal(result.isError, true); checks++;
  }
  const again = await client.callTool({ name: 'get_project_summary', arguments: { project: 'demo' } });
  assert.equal(again.structuredContent.open, 3); checks++;
  console.log(JSON.stringify({ checks, result: 'PASS', demo: demo.structuredContent }));
} finally {
  await client.close();
}
```

Run:

```bash
node client.mjs
```

The local test produced:

```json
{"checks":9,"result":"PASS","demo":{"project":"demo","open":3,"closed":7,"source":"fixture-v1"}}
```

The nine checks cover the catalogue, expected result, missing project, four problematic inputs and a new valid call after the errors. The client closes the connection in the `finally` block.

The client pins protocol revision `2026-07-28`: the test fails if the server does not support it. The number `2.3.1` identifies the SDK package instead. These versions need separate checks. For a hand-built v2 client, the documentation distinguishes the legacy default, `auto` mode that can negotiate an older path, and the pin used here, which enforces this revision. [SDK v2 compatibility](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28).

In the 24 October test, all nine checks passed both after installation from the lockfile with `npm ci` and through the `npm init` and `npm install` route. The next step is to connect a host and observe an actual call.

## Why the server uses stdio

With stdio, the client and server communicate through the process streams. It is a simple choice for this local example. The server does not open an HTTP port, and the client starts the process when needed.

Standard output is reserved for protocol messages. A debugging `console.log` in the server can corrupt the exchange. Use standard error for local diagnostics and keep confidential information out of logs.

For a remote service, assess Streamable HTTP and design authentication, deployment and connection handling alongside the transport. This tutorial stays local: the client starts Node directly. [SDK v2 stdio](https://ts.sdk.modelcontextprotocol.io/v2/serving/stdio.html).

### Connect it to Claude Code or Codex

After the local PASS, you can register the same server in either client. You need that client’s CLI installed and Node available in its environment. From the demo directory, run only the commands for your chosen client:

```bash
MCP_DEMO_SERVER="$PWD/server.mjs"

# Claude Code
claude mcp add --transport stdio project-summary -- node "$MCP_DEMO_SERVER"
claude mcp list

# Codex
codex mcp add project-summary -- node "$MCP_DEMO_SERVER"
codex mcp list
```

Open a client session, use `/mcp` to inspect the connection and tools, then ask it to call `get_project_summary` with `{"project":"demo"}`. Inspect the call and response: they should show `open: 3`, `closed: 7` and `source: "fixture-v1"`. A registration message means the configuration was saved; it does not yet establish a working connection. If the process fails to start, check the Node executable and absolute file path first. If it starts but the call fails, compare the error with the local test. Update the saved absolute path if you move the demo.

These recipes follow the client documentation and need checking in your environment; the execution above uses the JavaScript client. ChatGPT in a browser does not read local Codex configuration. [Claude Code configuration](https://code.claude.com/docs/en/mcp-quickstart), [MCP in Codex](https://learn.chatgpt.com/docs/extend/mcp?surface=cli).

## Which MCP tools on GitHub I would consider

A useful selection starts with the scenario and maintainer. Stars do not tell you which permissions are needed, whether a client supports a feature or how large a response is.

### GitHub MCP Server: repositories, issues and pull requests

This is GitHub's official server, with features for reading and operating on the platform. I would consider it when a workflow needs current repository, issue or PR information from a compatible host. The project documents remote and local modes. [github/github-mcp-server](https://github.com/github/github-mcp-server).

For one value in a script, I would first compare `gh` and the API. If the MCP server is needed, toolsets expose groups of functions and read-only mode excludes writes. This filter reduces the available operations; credential permissions still need configuring on the service. [Toolsets and read-only mode](https://github.com/github/github-mcp-server), [gh api](https://cli.github.com/manual/gh_api).

### Playwright MCP: browser interaction

Microsoft's project exposes browser automation through Playwright. It is a candidate when the agent needs to explore an interface or interact with page elements. [microsoft/playwright-mcp](https://github.com/microsoft/playwright-mcp).

The maintainers also offer [Playwright CLI with skills](https://github.com/microsoft/playwright-cli) for coding agents. Compare that route with MCP when the agent has a terminal; a Playwright script with deterministic assertions remains useful for regression tests of a fixed workflow. MCP is a candidate for iterative interface exploration using the server’s tools. In every case, consider which browser session, accounts and data you expose. The maintainers’ efficiency comparison is not a measurement of our workflow.

### Context7: documentation in the workflow

Context7, maintained by Upstash, offers both MCP and a CLI with skills for finding relevant library documentation. The choice depends on the tools available in your host and how you want to retrieve references. [upstash/context7](https://github.com/upstash/context7).

It is official for the Context7 project; that does not make it the producer of every indexed library. I would check the requested version, the snippet's provenance and whether it contains enough context. A short answer about the wrong version remains wrong.

### MCP Inspector: understand the contract

Inspector is a tool from the MCP project for examining the catalogue, parameters and responses before involving a model. It offers interactive interfaces and a scriptable CLI. The tutorial client instead contains nine assertions against our fixture’s expected result: keep those as contract tests and use Inspector to explore other calls. [modelcontextprotocol/inspector](https://github.com/modelcontextprotocol/inspector).

### Reference servers: study an implementation

The reference-servers repository collects educational implementations from the MCP project. The maintainers distinguish them from production integrations: they are useful for studying SDKs and the protocol. To choose an operational service, check its maintainer, licence, relevant issues and compatibility with your environment. [modelcontextprotocol/servers](https://github.com/modelcontextprotocol/servers).

## How to assess a server before adopting it

Choose a real task small enough to check the result. Record the tools selected and the data passing through them. A summary needs read access; changing an issue introduces a different effect. For a remote server, also assess the party receiving the data and executing the operation.

Then test failures that change the answer: missing data, denied access, timeouts and incomplete searches. An assistant given an empty list for all these cases cannot distinguish them. Access and error design return in the tool anatomy below. [MCP authorisation](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization).

## Anatomy of an MCP tool: from its name to authorization

In the catalogue example, “help me prepare a proposal” hides operations with different consequences: reading an item, saving a draft and sending it to a recipient. I would expose these actions separately. The process owner can then decide which to automate and where a check is needed; the server implementation can assign appropriate access and controls.

A tool has a descriptor, discovered by the client through `tools/list`, and an implementation on the server. The descriptor explains what can be requested; the code has to honour those promises. A `tools/call` request identifies the tool and its arguments. The server validates the request, checks access and carries out the operation. [Tools in the MCP specification](https://modelcontextprotocol.io/specification/2026-07-28/server/tools).

### Names, descriptions and schemas are part of the interface

`get_catalog_item` describes a more precise action than `manage_data`. I would use stable, recognisable names without inserting instructions such as “always call me”. The `title` supplies a readable label. The `description` should help the model choose: when to use the tool, what it returns, its effects and its relevant limits. A useful description might specify that a lookup covers only the connected account’s catalogue and does not calculate stock or customer-specific prices. This design approach follows Anthropic’s guidance on effective tools. [Writing effective tools](https://www.anthropic.com/engineering/writing-tools-for-agents).

In an MCP descriptor, `name` and `inputSchema` are required; `title`, `description`, `outputSchema`, `annotations`, `icons` and `_meta` are optional. Clients can impose additional requirements. `inputSchema` describes a JSON object; `outputSchema`, when present, describes the result in `structuredContent`. Icons support presentation. `_meta` holds metadata, including client-specific entries: its keys do not all have universal meanings. [Tool schema](https://modelcontextprotocol.io/specification/2026-07-28/schema#tool).

Back to the catalogue: the following descriptor designs a read within the authenticated account. It is not a second server to start; it lets us examine the contract field by field.

```json
{
  "name": "get_catalog_item",
  "title": "Read a catalogue item",
  "description": "Read one item from the authenticated account's catalogue. Returns approved product details. Does not calculate stock or customer-specific prices.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "itemId": {
        "type": "string",
        "minLength": 1,
        "maxLength": 64,
        "description": "The catalogue item identifier."
      }
    },
    "required": [
      "itemId"
    ],
    "additionalProperties": false
  },
  "outputSchema": {
    "type": "object",
    "properties": {
      "item": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "name",
          "description"
        ],
        "additionalProperties": false
      }
    },
    "required": [
      "item"
    ],
    "additionalProperties": false
  },
  "annotations": {
    "readOnlyHint": true,
    "destructiveHint": false,
    "openWorldHint": false
  }
}
```

The schema constrains the shape of the data. To decide whether a user may read `itemId`, the server must validate credentials and check that the item belongs to their account on every request. I would not accept an account identifier as proof of authorization. API keys should stay out of arguments, descriptions and results. OpenAI’s guide requires authorization to be enforced on the server. [Authentication and authorization](https://developers.openai.com/plugins/build/mcp-server#authenticate-and-authorize-requests).

For an OpenAI plugin, `securitySchemes` can declare the access mechanism, such as OAuth and its scopes. This integration setting is separate from MCP annotations; the server must still validate the token and enforce permissions. Server instructions can describe shared rules, such as reading a version before updating it. They do not replace individual tool descriptions. [OpenAI MCP server guide](https://developers.openai.com/plugins/build/mcp-server).

### Input schemas: fields, constraints and defaults

In the JSON, `properties` defines validation for the named fields, `required` requires `itemId`, and `minLength` and `maxLength` constrain its length. `additionalProperties: false` excludes extra fields; `properties` alone does not ban them. An absent field and a field containing `null` are different cases. [Objects in JSON Schema](https://json-schema.org/understanding-json-schema/reference/object).

I would also specify units, enumerated values and the meaning of identifiers where needed. A quantity, for instance, should distinguish items from packs; an amount needs a currency. A validator cannot infer these constraints from a vague description. The `default` keyword documents a value, but JSON Schema validation does not automatically insert it when a field is missing. [JSON Schema annotations](https://json-schema.org/understanding-json-schema/reference/annotations).

An SDK may offer shorter syntax, such as the tutorial’s Zod objects, and generate the descriptor exposed to the client. In the final check, I would therefore inspect `tools/list` as well as the registration code: that is the contract the client receives. I would not add a “confirm: true” parameter and trust the model to fill it in: a model-generated argument is not evidence of a person’s consent.

For input validity, Claude Code also documents top-level property names of 1–64 characters using ASCII letters, digits, `_`, `.` and `-`, plus 2020-12 schema checks. Exclusion of invalid tools depends on the documented feature flags; root-level combinators have separate rewriting behaviour. I would therefore test the schema in the host actually deployed. [Claude Code schema validation](https://code.claude.com/docs/en/mcp#tools-with-invalid-input-schemas).

### The four annotations: meanings, defaults and limits

Annotations provide hints about behaviour. The `Hint` suffix matters: declaring a read does not prevent code from writing or grant access to a document. The client has to apply its own rules and consider whether it trusts the server. [Why MCP introduced tool annotations](https://blog.modelcontextprotocol.io/posts/2026-03-16-tool-annotations/).

_The four annotations: meanings, defaults and limits_

| Annotation | What it describes | Protocol default if omitted |
| --- | --- | --- |
| readOnlyHint | true: the tool does not modify its environment’s state. Reading confidential data still requires authorization. | false |
| destructiveHint | For a tool that writes, true signals potentially destructive updates; false describes purely additive behaviour. | true |
| idempotentHint | For a tool that writes, true means repeating the same call with the same arguments creates no additional effects. | false |
| openWorldHint | true: interaction with open-world entities; false: activity bounded by a closed domain. | true |

The defaults describe how to interpret an omitted annotation. `destructiveHint` and `idempotentHint` apply to writes, when `readOnlyHint` is `false`; this is why the read-only fixture tool does not declare idempotency. The readable label uses `title` first, then `annotations.title`, and finally `name`. The host’s confirmation policy is covered next. [ToolAnnotations schema](https://modelcontextprotocol.io/specification/2026-07-28/schema#toolannotations).

Idempotency concerns effects: a second attempt with the same arguments must not create another draft or send a second message. Responses can change, for example when a read finds updated data. For a creation operation, an idempotency key lets the service recognise a request completed before a timeout; the service must store and enforce that key. This has a concrete client consequence: Claude Code documents a possible retry after reconnecting interrupted HTTP or connector calls marked `idempotentHint: true` or `readOnlyHint: true`. Declaring the hint without handling retries can therefore expose duplicate effects. [Claude Code retries](https://code.claude.com/docs/en/mcp).

“External” does not simply mean “hosted on another server”. OpenAI’s guidance allows a bounded private workspace to use `openWorldHint: false` even when it is a remote SaaS service. Public web search uses `openWorldHint: true` despite being read-only. The boundary concerns the entities the tool can reach. [Annotations in OpenAI’s guide](https://developers.openai.com/plugins/build/mcp-server#tool-annotations-and-elicitation).

For the illustrative process, I would classify the operations as follows:

_The four annotations: meanings, defaults and limits_

| Operation and assumed boundary | readOnlyHint / openWorldHint | destructiveHint |
| --- | --- | --- |
| Read an item only from the connected private catalogue | true / false | false |
| Search the public web | true / true | false |
| Add a new workspace draft without overwriting anything | false / false | false |
| Send a proposal to an unrestricted external address, with no way to recall it | false / true | true |

The same label can hide different effects. A “save draft” action that overwrites a previous version changes the assessment of `destructiveHint`. A “send” action restricted to a closed recipient list changes the assessment of `openWorldHint`. For `idempotentHint`, what matters is what happens when the service receives the same arguments twice.

### Who asks for approval: ChatGPT, Claude Code and Codex

None of the four annotations means “always ask for confirmation”. OpenAI’s plugin guidelines require explicit booleans for `readOnlyHint`, `destructiveHint` and `openWorldHint`: a protocol-valid descriptor can therefore be incomplete for that integration route. Names and descriptions must also match the actual operation. [Plugin guidelines](https://developers.openai.com/plugins/plugin-guidelines).

For custom MCP servers in ChatGPT, the documentation specifies confirmation of writes by default and treats a tool without `readOnlyHint` as a write. Choices remembered within the conversation can change subsequent behaviour. This describes ChatGPT, rather than a universal MCP rule. [Custom MCP servers in ChatGPT](https://developers.openai.com/api/docs/guides/custom-mcp-server).

Claude Code supports permission rules for tools identified as `mcp__server__tool`: calls can be allowed, require a prompt or be denied. For this example, I would require a prompt for sending instead of allowing the entire server. That is client policy, separate from access to the data. [MCP permissions in Claude Code](https://code.claude.com/docs/en/permissions#mcp).

Claude Code also documents `_meta["anthropic/requiresUserInteraction"]: true`, requiring human interaction on each call; this is not a standard annotation. `dontAsk` denies the tool. With the Agent SDK, the `canUseTool` callback can approve, and the application must present the prompt. A full prefixed name exceeding 128 characters causes the tool to be omitted. [MCP in Claude Code](https://code.claude.com/docs/en/mcp).

Codex supports a server-level policy and a different policy for an individual tool. Open `~/.codex/config.toml` and find `[mcp_servers.project-summary]`, the table created by the earlier registration. Insert `default_tools_approval_mode` into that table, keeping `command` and `args`; do not duplicate its header. Then add the per-tool subtable shown below. The example prompts at the server level and separately approves the known fixture read.

```toml
[mcp_servers.project-summary]
default_tools_approval_mode = "prompt"

[mcp_servers.project-summary.tools.get_project_summary]
approval_mode = "approve"
```

The documented server modes are `auto`, `prompt`, `writes` and `approve`; `writes` prompts for tools not marked read-only. A per-tool rule overrides the server policy. `enabled_tools` and `disabled_tools` instead filter availability: approving a call and making a tool available are separate controls. I would retain `prompt` for a future sending tool. [MCP in Codex](https://learn.chatgpt.com/docs/extend/mcp?surface=cli), [configuration reference](https://learn.chatgpt.com/docs/config-file/config-reference).

If you build an application with the Responses API, `require_approval` belongs to yet another surface: its documentation provides approval requests before sharing data with remote servers. It does not inherit Codex configuration or ChatGPT conversation settings. [MCP in the Responses API](https://developers.openai.com/api/docs/guides/tools-connectors-mcp#approvals).

For sending, I recommend confirmation that shows the recipient and content and is bound to the action the server will execute. Authentication, data authorisation and approval of an individual action answer different questions: who you are, what you may do and which operation you approved.

### Results and errors complete the contract

`structuredContent` carries structured data; `content` can contain text or other MCP content. When declared, `outputSchema` describes the tool’s structured result, rather than the model’s final written answer. An operation failure can return `isError: true`; protocol errors are a separate case. [Tool results and errors](https://modelcontextprotocol.io/specification/2026-07-28/server/tools).

For OpenAI plugins, the reference also requires `outputSchema` when a tool returns `structuredContent`. That integration requirement is stricter than the protocol’s optional field. [OpenAI tool descriptor parameters](https://developers.openai.com/plugins/reference#tool-descriptor-parameters).

In the catalogue, “no item found” and “service unavailable” need different responses. I would not fill the latter with an empty list: the assistant might interpret it as an absence of products. I would return stable identifiers and, where the decision needs them, the source, retrieval time and result boundaries. A paginated list should make it clear whether more pages exist.

OpenAI’s guide also distinguishes result `_meta`, hidden from the model, from model-readable content. Hiding a field from the model does not make it secure credential storage: the client can receive it. This container should not be confused with the descriptor `_meta` used for Claude Code above. [Useful results without custom UI](https://developers.openai.com/plugins/build/mcp-server#return-useful-results-without-ui).

### Discovery, names and versions: why a tool may be missing

The specification recommends server-scoped unique names of 1–128 characters using ASCII letters, digits, `_`, `-` and `.`, without spaces. A readable title can be a phrase. Keep the name short: a host may add a prefix and enforce its own limit, such as the Claude Code limit above. [MCP tool names](https://modelcontextprotocol.io/specification/2026-07-28/server/tools#tool-names).

The tool list can be paginated: `nextCursor` continues discovery, and the client must treat it as opaque. This pagination concerns the function catalogue. Pagination of products returned by our own search belongs in that search’s contract; MCP does not add it automatically. [Protocol pagination](https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/pagination).

In revision `2026-07-28`, `tools/list` also includes `ttlMs` and `cacheScope`, governing catalogue-cache freshness and sharing. If the list depends on the user, the cache boundary must respect that. These fields are not an automatic cache for `tools/call` results or the same thing as model prompt caching. [MCP caching](https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching).

Revision `2026-07-28` requires `protocolVersion` and `clientCapabilities` in every request’s metadata. `clientInfo` is optional and recommended; this self-reported client identity is unsuitable for authenticating the caller. Earlier `initialize`-based revisions use a handshake instead. [Base metadata](https://modelcontextprotocol.io/specification/2026-07-28/basic), [versioning and compatibility](https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning).

### A complete response, with data and readable content

For the catalogue descriptor, an illustrative success payload is:

```json
{
  "resultType": "complete",
  "structuredContent": {
    "item": {
      "id": "item-42",
      "name": "Adjustable desk",
      "description": "Height-adjustable work surface."
    }
  },
  "content": [
    {
      "type": "text",
      "text": "{\"item\":{\"id\":\"item-42\",\"name\":\"Adjustable desk\",\"description\":\"Height-adjustable work surface.\"}}"
    }
  ],
  "isError": false
}
```

This is the result payload on the wire, without the JSON-RPC envelope and request identifier. `resultType: "complete"` belongs to revision `2026-07-28`; the SDK manages this transport bookkeeping, so you do not need to copy it into the fixture’s handler. In that revision, `structuredContent` can be any JSON value allowed by the output schema. [Result schema](https://modelcontextprotocol.io/specification/2026-07-28/schema#calltoolresult), [SDK and wire payloads](https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28).

`content` can include text, images, audio, embedded resources and resource links; actual support needs testing in the client. Text is sufficient for our catalogue. A resource link and the resource’s complete contents represent different transfer choices: sending everything “just in case” increases the material to handle. [MCP content types](https://modelcontextprotocol.io/specification/2026-07-28/schema#contentblock).

There is another naming overlap to watch: content annotations such as `audience`, `priority` and `lastModified` describe intended recipients, priority and modification time. They are separate from the tool’s four behavioural annotations and do not enforce access. [Resource and content annotations](https://modelcontextprotocol.io/specification/2026-07-28/server/resources#annotations).

### Data access, requests for information and long-running work

When you adopt MCP’s specified HTTP authorisation mechanism, its flow is OAuth-based. That mechanism is optional in the protocol; this does not make an authentication-protected service’s data public. For stdio, the specification points to credentials obtained from the environment. The catalogue needs scopes restricted to the necessary operations and account checks on each access, including IDs the server returned earlier. [MCP authorisation](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization).

Elicitation collects missing information through the client, such as the proposal’s language. It does not amount to permission to send it. Form mode must not collect passwords or tokens; the specification provides URL mode for sensitive interactions, subject to declared client support. [MCP elicitation](https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation).

In the current revision, an `input_required` result can request the inputs needed before retrying the request. The server cannot assume the user will respond; it must protect the integrity of any `requestState` that affects authorization or operations. [Multi round-trip requests](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr).

For long-running work, the Tasks extension can return a job identifier and let the client follow progress, if both parties support it. Cancellation is cooperative: closing the chat does not necessarily stop a send or a change already in progress. [Tasks extension](https://modelcontextprotocol.io/extensions/tasks/overview).

### Client metadata and transport: beyond the four annotations

A graphical interface is optional. To connect one to a tool, OpenAI’s documentation also uses MCP Apps metadata such as `_meta.ui.resourceUri` and `_meta.ui.visibility`; some `openai/...` keys serve compatibility or product features. None automatically grants data permissions. [OpenAI integration reference](https://developers.openai.com/plugins/reference#_meta-fields-on-tool-descriptor).

Metadata can belong to the request, descriptor or result: placement changes meaning. I would not move a key between containers simply because they share its name. For example, Claude Code’s human-interaction flag belongs on the descriptor; UI-only data follows the result contract.

For Streamable HTTP, revision `2026-07-28` lets a parameter use `x-mcp-header` to mirror its value into a `Mcp-Param-...` header, for example for routing. Constraints cover names, types and schema placement; on this transport an invalid definition requires the tool to be excluded. Intermediaries can see these headers, so do not use this mechanism to transmit secrets. [Parameters in HTTP headers](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#custom-headers-from-tool-parameters).

### Check the tool in the intended client

Before adoption, I would test the full path: discovery in `tools/list`, a valid call, malformed input, missing credentials, an attempt to read another account’s item, service failure and a retry after a timeout. For writes, I would also refuse approval and check that no effect occurred. Finally, I would compare tool selection from realistic requests in the clients being supported.

Keep the `tools/list` output with the server version. After an update, compare descriptions, schemas and annotations too: a tool still called “search” may have changed what its filters mean. Rerunning the same question against an expected result helps catch that before using it in the workflow again.

## A partial result must declare its boundaries

Imagine a tool that reads only the first page of issues. Its `items` field should be accompanied by `has_more` and the next cursor; include a total count only if the service knows it. The retrieval timestamp also matters when the project changes.

To assess an optimisation, I would compare complete and filtered responses to the same question, checking that they preserve the necessary information. I would record definitions actually loaded, outputs, retries, tokens and time until an accepted result, including the external script’s work. That comparison is needed before attributing a saving to MCP; the executed test here concerns the fixture’s behaviour.

Provenance also helps separate content from instructions: an issue or external page may contain text attempting to direct the agent. I would not give it the permissions of an approved skill. Application controls must restrict access even when the model misinterprets content.

## When to add a skill

The demonstration server returns counts. It does not determine how to prepare a project update, which sources to compare or how to report a gap. A skill can describe that procedure.

I would add one when the workflow recurs and includes decisions that should be consistent: identify the project, read the summary, check whether the source is sufficient, and present the result with its provenance. A skill can use MCP alongside other tools; it does not necessarily require a server.

I would keep the description precise enough to avoid activation for any question about issues. The dedicated skills article develops the complete design and differences between clients.

Skills over MCP is a published optional extension for distributing instructions and supporting files; check host support before choosing it as your distribution channel. OpenAI’s documented plugin route is specific: `Scan Tools` imports a snapshot using `skills/list`, `skills/get` and `resources/read`, with a complete manifest and SHA-256 digests. A change requires a new scan and plugin version. A submission snapshot does not refresh from the server on each conversation. [Skills over MCP](https://modelcontextprotocol.io/extensions/skills/overview), [OpenAI import](https://developers.openai.com/plugins/build/mcp-server#import-skills-from-the-mcp-server).

## How to limit token consumption with MCP

The first opportunity is to improve the request sent to the server. If you need five blocked issues, retrieving all issues and filtering in the conversation transfers material that could have been excluded earlier.

The second is to design responses with clear boundaries: relevant fields, limits, pagination and an indication of omitted results. A summary must preserve the data needed for the decision. Anthropic's work on tools emphasises comprehensible interfaces and task-based evaluation. [Writing effective tools for agents](https://www.anthropic.com/engineering/writing-tools-for-agents).

How the host loads tools matters too. Claude Code documents deferred tool discovery, which avoids loading every definition upfront; configuration, model and availability can change that path. Installing a server therefore does not mean every description enters every request in full. External data processing is another option: code can filter a result before returning it to the model. [Claude Code tool search](https://code.claude.com/docs/en/mcp#scale-with-mcp-tool-search), [code execution with MCP](https://www.anthropic.com/engineering/code-execution-with-mcp).

Our tool returns four fields because they answer the chosen question. If tomorrow's task is to understand which issues are blocked and why, those four fields would be insufficient. The aim is to provide the context needed with fewer unnecessary steps.

## Where to start in your own project

Choose an operation whose result you can verify, constrain the input and make output provenance checkable. Verify success and errors with a client before connecting a model. Then compare the same task with the direct alternative and count the entire path to the result.

If MCP simplifies reuse and clarifies boundaries, you have a reason to adopt it. If it adds a process without improving the work, the original script may still be the right solution.
