The best practices for MCP server design come down to small tools, one job each, with read and write kept apart. Give every tool a plain description, a strict input schema and results a person can check. Enforce permissions and approvals in the server, never in the prompt. Sign people in with OAuth so each call runs as a named user, and log every call.
How should you split an MCP server into tools?
Start from the jobs, not the API. List what a person actually does in the system and make each job a tool. Sixty endpoints usually become eight tools.
- One job per tool. "Find a member by email" and "create a room booking" are tools. "Call the Nexudus API" is not.
- Read and write are separate tools. You can grant, approve and log them differently.
- Name tools as verbs, such as
search_contactsorcreate_draft_email. The MCP spec recommends names of 1 to 128 characters using letters, digits, underscore, hyphen and dot. - Keep the list short. Every tool's schema is sent to the model, so fifty tools cost context and make the right one harder to pick.
- Mark behavior with annotations such as
readOnlyHintanddestructiveHint. Clients use them to decide what to confirm, but the spec says to treat them as untrusted, so they never replace a check in your code.
How do you write schemas and results a model can use?
- Constrain inputs. Required fields, enums, formats and maximums. No free-form filter strings passed through to a database.
- Describe each field for the model: units, formats, examples. "Date as YYYY-MM-DD, must be in the future" prevents a retry.
- Return structured results. Declare an
outputSchemaand returnstructuredContent, plus the same data as text for older clients, as the spec advises. - Return enough to verify. Record IDs, source names and dates, so a person can check the answer without re-running it.
- Cap and page large results rather than returning ten thousand rows into the conversation.
Errors matter as much as results. The spec separates protocol errors, such as an unknown tool, from tool execution errors, which come back in the result with isError: true so the model can read them and correct itself. Write those messages for the model: "No member with that email. Try search_members with the company name" gets a second attempt right. "Error 400" does not.
What changed for server design in the 2026-07-28 MCP spec?
The revision published July 28, 2026 made MCP stateless. Three consequences:
- No sessions. The initialize handshake and the
Mcp-Session-Idheader are gone, and each request carries its own protocol version and client capabilities. If a workflow needs state, such as a draft being built over several calls, return a handle from one tool and accept it as an argument on the next. Check the caller's authorization against that handle on every call. - The tool list may vary by caller's authorization, not by connection. It is fine to return only the tools a person's scopes allow. Return them in a stable order so clients can cache the list.
- Some features are deprecated. Roots, sampling and logging are marked deprecated, with at least a twelve-month window before removal. Pass folders or files as tool parameters instead of relying on roots, and log with OpenTelemetry or to stderr.
Long-running jobs now belong to the optional Tasks extension, and interactive UI to the MCP Apps extension. Both need support on the client side, so check before depending on them.
How should sign-in and permissions work?
For a remote server used by a team, use OAuth so every call runs as a named person with that person's permissions. The spec requires:
- The server is an OAuth 2.1 resource server and must publish Protected Resource Metadata (RFC 9728) naming its authorization server.
- It answers a request without a valid token with a 401 and a
WWW-Authenticateheader, and a token with too little scope with a 403 anderror="insufficient_scope", so the client can ask for more. - It must accept only tokens issued for itself (the audience check) and must never pass a client's token through to another API.
- For client registration, Client ID Metadata Documents are now preferred, and Dynamic Client Registration is deprecated but still allowed.
For internal tools: Claude's custom connectors need the server reachable from the public internet, so authentication is not optional. Your identity provider, such as Microsoft Entra or Google, can do the sign-in, with an allowlist in the server deciding who gets in.
Where do approval and audit go?
In the server. A prompt that says "always ask before sending" is a request. A tool that can only create drafts is a guarantee.
- Draft instead of send for anything that leaves the company.
- A dry-run flag on writes to physical or financial systems, on by default.
- Confirmation through the client. A server can pause a call and ask the person for confirmation or a missing value (elicitation), where the client supports it.
- Idempotency keys on creates, so a retried call does not make two records.
- A log line per call: who, which tool, a summary of the inputs, the system touched, the result, the approval state and a correlation ID. Keep secrets and full personal data out of it.
For spARK Labs we built this shape: one remote server across four tools behind Microsoft sign-in and an email allowlist, with a dry-run flag on building access and Outlook limited to drafts (case study). The Cloudflare Workers deployment is in our architecture write-up.
How do you test an MCP server before a team uses it?
- Call every tool directly with the MCP Inspector or a script: success, bad input, empty result, permission denied, upstream timeout.
- Try to break the boundary. Ask for another person's record. Retry a create. Put an instruction inside a record the model will read and check the dangerous tool still refuses.
- Test in the real clients your team uses, such as Claude, Cowork and Claude Code. Support for transports, sign-in and features differs between clients and between releases.
- Run ten real tasks end to end and read the logs, not just the answers.
Common questions
How many tools should an MCP server have?
As few as cover the real jobs, which for one business system is often five to fifteen. Split by job and permission level, not by API endpoint.
Does an internal MCP server have to be on the public internet?
For Claude's custom connectors, yes: Anthropic connects from its own infrastructure, so the server must be reachable over the internet and should require OAuth sign-in. A local server running on each person's machine is the alternative, but it gives up central updates, logging and sign-in.
What did the 2026-07-28 MCP spec change for server builders?
It removed protocol sessions and the initialize handshake, so each request stands alone and any state across calls is carried in explicit handles passed as tool arguments. It also deprecated roots, sampling and logging, and moved long-running tasks into an optional extension.

