TellWorldAI World Archive
Ask中文

Home›Practice›Practice

Practice

MCP server best practices

Learn how to split MCP tools, validate parameters, handle errors, control access and rate limits, and why side-effecting tools need careful retry design.

Who this is for and what you will be able to do

This guide is for developers who have written an MCP tool and are preparing a server for other users or for production. After reading it you should be able to check your server against a short list: whether each tool has one job, whether parameters are validated, whether error messages are actionable, whether permissions and rate limits are in place, and whether side-effecting operations are designed for safe retries.

Start with a few principles

Five core principles guide the design. Communication is standardized on JSON-RPC 2.0. Design is user-centric, giving priority to consent, control and transparency. Security comes first. The architecture is modular, with each tool and resource serving a clear, focused purpose. State is explicit[1].

The last point needs attention. MCP 2026-07-28 is stateless at the protocol layer. If a workflow needs state across several calls, pass explicit handles or ordinary tool arguments, backed by durable application state[1]. Do not expect the protocol to remember the previous result for you.

Security: the first things to do

The official security practices begin with consent. Get explicit user consent before accessing data or performing operations, and before invoking any tool[1][2].

The list continues with specific requirements[1]:

  • Authentication: before granting access to tools, resources or sensitive operations, authenticate the caller with API keys, OAuth tokens or another secure method.
  • Parameter validation: validate every tool invocation so that malformed or malicious input never reaches the implementation.
  • Rate limiting: cap the request rate to prevent abuse and keep server resources fairly shared.
  • Tool permission control: for each request and authorization context, decide which tools the model may use, and allow only explicitly authorized tools.

The OWASP MCP Top 10, which covers security risks and mitigations, is also worth reading[1].

Tool design: one tool, one job

Each tool should have a clear, focused purpose. A monolithic tool that tries to do everything makes its parameters complicated and makes it harder for a model to choose the right call[1]. This follows directly from the modular principle.

Parameter validation: check in order

Validation has to be thorough. The JavaScript example below checks allowed values, here confirming that the operation is one of the permitted set before the rest of the checks run[1]:

    // 3. Validate parameter values
    const validOperations = ["read", "write", "delete"];
    if (!validOperations.includes(parameters.operation)) {
      throw new ToolError(`Invalid operation. Must be one of: ${validOperations.join(", ")}`);
    }

The full order in that example is presence, then type, then allowed values, then required content for write operations, and finally path safety[1]. In the file-operation example, operation is an enum of read, write and delete, and path must be inside allowed directories[1].

Error handling: separate the cases, give useful messages

Standardized error messages and codes help people diagnose problems, let failures be handled gracefully, and give actionable feedback[1]. The Python example below handles timeouts, connection errors and query errors separately, raising a tool error with a different message for each. The database call has a 10 second timeout[1]:

            except asyncio.TimeoutError:
                raise ToolExecutionError("Database query timed out after 10 seconds")
            except DatabaseConnectionError as e:
                # Connection errors might be transient
                self._log_error("Database connection error", e)
                raise ToolExecutionError(f"Database connection error: {str(e)}")
            except DatabaseQueryError as e:
                # Query errors are likely client errors
                self._log_error("Database query error", e)
                raise ToolExecutionError(f"Invalid query: {str(e)}")

The comments give the reasoning. A connection error may be transient, while a query error most likely comes from the caller's input. With that split, the caller can decide whether to retry or to change the request.

Rate limits and authentication in practice

One example is a C# rate-limiting middleware. It reads the per-minute limit from RateLimit:MaxRequestsPerMinute, with a default of 60[1]. It counts requests for each client identifier, an API key or user ID, within each minute. When the limit is exceeded, it returns HTTP 429 with a Retry-After header of 60. Normal responses carry X-RateLimit-Limit and X-RateLimit-Remaining headers[1].

The authentication example is written in Java. It validates the auth token first, then checks authorization for the specific data ID and operation, and only then performs the read or update[1]. The order matters: authentication comes first and authorization follows immediately.

Observability: send logs to stderr

With stdio transport, standard output carries protocol messages, so diagnostics should go to stderr. For structured observability, use OpenTelemetry. The 2026-07-28 specification marks MCP's built-in logging feature as deprecated[1].

Do not retry side-effecting tools blindly

A generic retry loop is unsafe for tools that create tickets, process payments, send messages or trigger deployments. A response can be lost after the effect has already been committed. The client then assumes failure and retries, and the action happens twice[1].

These tools need more care. Use stable operation keys so duplicates are not admitted, checkpoint progress, and reconcile with the external system. A companion reliability lesson covers these topics[1].

Long-running work: report progress and allow cancellation

For long operations, servers should report progress so that user interfaces stay responsive. Clients should also be able to cancel requests that are no longer needed or that take too long[1].

Limits and common questions

  • Most examples are fragments. The helper _log_error is left unimplemented, so copying them as they are will not run. Understand the idea before you write your own version.
  • Best practices change with the specification. This guide uses 2026-07-28 as its baseline. Older revisions handle the handshake and logging differently, so check the current specification during migration[1][2].
  • The numbers in the examples, such as 60 requests per minute and a 10 second timeout, are sample values. Set real limits from your service's load and backend capacity.

Next step

After this guide, read the testing and workflow design sections of this series and apply these principles to your test cases. If your server performs side-effecting actions, read the companion reliability lesson first[1].

Adapted from the best-practices lesson of Microsoft's open-source course *MCP for Beginners*[1], released under the MIT License.

Sources

  1. 1
    MCP Development Best PracticesMicrosoft mcp-for-beginners (MIT)
  2. 2
    Security Best PracticesModel Context Protocol

Cite this page: MCP server best practices. TellWorld, https://www.tellworld.net/practice/practice-mcp-best-practices. Updated 2026-11-14

Related news

View all