<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Trexmi</title>
    <description>The latest articles on DEV Community by Trexmi (@trexmi_tools).</description>
    <link>https://dev.to/trexmi_tools</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4111991%2Fbae7096e-84a5-4b11-b869-20169bebe7d6.png</url>
      <title>DEV Community: Trexmi</title>
      <link>https://dev.to/trexmi_tools</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/trexmi_tools"/>
    <language>en</language>
    <item>
      <title>How to Build and Validate an MCP Tool Schema</title>
      <dc:creator>Trexmi</dc:creator>
      <pubDate>Sun, 06 Sep 2026 08:22:11 +0000</pubDate>
      <link>https://dev.to/trexmi_tools/how-to-build-and-validate-an-mcp-tool-schema-1bmb</link>
      <guid>https://dev.to/trexmi_tools/how-to-build-and-validate-an-mcp-tool-schema-1bmb</guid>
      <description>&lt;p&gt;An MCP tool schema is more than a JSON object that passes a parser. It is the contract a client and a language model use to discover a capability, choose it at the right time, construct arguments, and interpret the result.&lt;/p&gt;

&lt;p&gt;A weak contract may still look valid while producing bad calls. A vague description can make the model choose the wrong tool. An over-strict &lt;code&gt;required&lt;/code&gt; list can block useful requests. An open-ended object can silently accept misspelled fields. A schema also cannot prove that the handler is authorized, safe, or correct.&lt;/p&gt;

&lt;p&gt;This guide follows the Model Context Protocol tools specification dated 2026-07-28 and JSON Schema Draft 2020-12. That revision is the largest rework of the protocol since launch, and it changed enough around tool definitions that several older guides are now incomplete. We will build one tool definition, improve it deliberately, and finish with a validation workflow suitable for production work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the portable core
&lt;/h2&gt;

&lt;p&gt;A practical MCP tool definition begins with a stable name, a precise description, and an &lt;code&gt;inputSchema&lt;/code&gt; describing the call arguments.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"search_docs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Search product documentation and return the most relevant passages."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inputSchema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://json-schema.org/draft/2020-12/schema"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"integer"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"include_archived"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"boolean"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"additionalProperties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The MCP specification also supports optional fields such as a display &lt;code&gt;title&lt;/code&gt;, icons, &lt;code&gt;outputSchema&lt;/code&gt;, annotations, and extension metadata. Add them when the server and its clients can use them. Do not guess them merely to make the definition look complete.&lt;/p&gt;

&lt;p&gt;If you want a deterministic starting point, the &lt;a href="https://trexmi.com/tools/mcp-tool-schema-generator/" rel="noopener noreferrer"&gt;MCP Tool Schema Generator&lt;/a&gt; converts a representative JSON object into a minimal tool definition. It infers property types and nested shapes without calling an AI model, runs entirely in the browser, and needs no account. Treat the result as editable source, not a finished contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose a stable tool name
&lt;/h2&gt;

&lt;p&gt;The current MCP guidance recommends names between 1 and 128 characters, treated as case-sensitive, using ASCII letters, digits, underscores, hyphens, or dots. A name should also be unique within its server.&lt;/p&gt;

&lt;p&gt;Good names identify one action:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;search_docs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;orders.get_status&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;create_support_ticket&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Names such as &lt;code&gt;helper&lt;/code&gt;, &lt;code&gt;process&lt;/code&gt;, or &lt;code&gt;run&lt;/code&gt; do not communicate enough intent. Avoid encoding a version in the name unless old and new contracts must remain available at the same time.&lt;/p&gt;

&lt;p&gt;Two details make name stability more important than it used to be.&lt;/p&gt;

&lt;p&gt;First, uniqueness is guaranteed only inside a single server. A client or proxy that aggregates several servers can easily end up with two &lt;code&gt;search&lt;/code&gt; tools, and the specification expects it to disambiguate — typically by prefixing a server identifier. It also warns against relying on the server's own reported name for that purpose, since it is not guaranteed unique. If your tool is likely to be aggregated, a name that already reads as domain-specific will survive better than a generic one.&lt;/p&gt;

&lt;p&gt;Second, &lt;code&gt;tools/list&lt;/code&gt; results are now cacheable. A server can advertise a lifetime and a cache scope, and clients may hold the tool set for as long as that permits. A renamed tool therefore does not just break saved configurations, prompts, and allowlists at the moment you deploy it — it can stay broken in caches for a while afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write a description that supports tool selection
&lt;/h2&gt;

&lt;p&gt;The schema validates arguments, but the description helps the model decide whether it should call the tool at all. A useful description answers three questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What action does the tool perform?&lt;/li&gt;
&lt;li&gt;What does it return?&lt;/li&gt;
&lt;li&gt;When should another tool be preferred?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Compare these descriptions:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Search documentation.&lt;br&gt;
Versus:&lt;br&gt;
Search public product documentation for matching passages. Returns document titles, URLs, and short excerpts. Use &lt;code&gt;get_document&lt;/code&gt; when the caller already has an exact document ID.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The second version separates discovery from retrieval and states the output shape. Keep behavioral rules in the description, but put machine-checkable limits in the schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use an object schema for named arguments
&lt;/h2&gt;

&lt;p&gt;Tool calls send named arguments, so an object root is the interoperable default. Even a no-argument tool should publish an object schema.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"get_current_time"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Return the current server time in UTC."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inputSchema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"additionalProperties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That closed form is the one the specification recommends for parameterless tools. A bare &lt;code&gt;{"type": "object"}&lt;/code&gt; is also valid, but it accepts any object, including one carrying properties you never declared.&lt;/p&gt;

&lt;p&gt;MCP uses JSON Schema Draft 2020-12 by default when &lt;code&gt;$schema&lt;/code&gt; is absent. Declaring the dialect explicitly can still help reviewers and tooling understand which rules you intended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate observed examples from real requirements
&lt;/h2&gt;

&lt;p&gt;Generating a schema from sample JSON is useful, but an example cannot reveal business intent. Consider this sample:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"refund policy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"include_archived"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It shows three property names and their observed types. It does not prove that all three are required, that &lt;code&gt;limit&lt;/code&gt; can be any integer, or that an empty query is useful.&lt;/p&gt;

&lt;p&gt;A reviewed schema might instead use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://json-schema.org/draft/2020-12/schema"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Words or phrase to find in the documentation."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"minLength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"maxLength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"integer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Maximum number of passages to return."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"minimum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"maximum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"include_archived"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"boolean"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Include archived documentation in the search."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"additionalProperties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only &lt;code&gt;query&lt;/code&gt; is required. The optional fields have defaults, and the numeric range prevents an accidental request for millions of results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Close object shapes intentionally
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;additionalProperties: false&lt;/code&gt; rejects undeclared keys. That is valuable for tool arguments because it catches mistakes such as &lt;code&gt;include_archive&lt;/code&gt; instead of &lt;code&gt;include_archived&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Strict objects also create a maintenance obligation. Adding a new field changes the accepted contract, so update the schema, handler, tests, and documentation together. For nested objects, decide separately whether each level should be closed.&lt;/p&gt;

&lt;p&gt;Do not use &lt;code&gt;additionalProperties: false&lt;/code&gt; automatically when arbitrary keys are part of the feature. A metadata map may legitimately accept user-defined property names. In that case, describe and constrain its values instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review arrays and nullable values manually
&lt;/h2&gt;

&lt;p&gt;Sample-based inference is weakest around arrays and &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The first item in an array does not prove that every later item has the same shape. An empty array gives no evidence about &lt;code&gt;items&lt;/code&gt;. A &lt;code&gt;null&lt;/code&gt; example does not reveal the intended non-null type.&lt;/p&gt;

&lt;p&gt;For a list of filters, describe the item contract and place useful bounds on the collection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"array"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"enum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"guide"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"reference"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"changelog"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"minItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"uniqueItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a field accepts a string or null, express both types only when the handler actually supports both. Do not add nullability as a defensive habit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add an output schema when structured results matter
&lt;/h2&gt;

&lt;p&gt;MCP tools may declare an optional &lt;code&gt;outputSchema&lt;/code&gt;. When it is present, the server must return conforming structured data and the client should validate it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"matches"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"array"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uri"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"excerpt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"excerpt"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"additionalProperties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"matches"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"additionalProperties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two practical notes that are easy to miss.&lt;/p&gt;

&lt;p&gt;An output schema does not have to describe an object. Structured results may be any JSON value, and an array root is explicitly supported, so a &lt;code&gt;list_users&lt;/code&gt; tool can declare an array of user objects directly rather than wrapping them in a single-key envelope. Wrap them only if the envelope earns its place — for example, because you expect to add pagination metadata later.&lt;/p&gt;

&lt;p&gt;Structured results travel in a dedicated field, but for backwards compatibility a tool that returns them should also place the serialized JSON in a text content block. Clients that predate structured content still receive something usable.&lt;/p&gt;

&lt;p&gt;An output schema improves validation, typed integrations, and documentation. It does not sanitize the returned content or make an untrusted URL safe. Output validation and output sanitization solve different problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mirror parameters into headers deliberately
&lt;/h2&gt;

&lt;p&gt;The 2026-07-28 revision introduced &lt;code&gt;x-mcp-header&lt;/code&gt;, an extension property placed directly inside a property's schema. It asks the client to copy that argument's value into an HTTP header named &lt;code&gt;Mcp-Param-{name}&lt;/code&gt; when the Streamable HTTP transport is used, so load balancers, proxies, and firewalls can route on it without parsing the request body.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"execute_query"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Run a read-only analytics query in a specific region."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inputSchema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"region"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Region the query executes in."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"enum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"us-west1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eu-west1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ap-south1"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"x-mcp-header"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Region"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Read-only SQL statement to execute."&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"region"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"additionalProperties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Called with &lt;code&gt;"region": "eu-west1"&lt;/code&gt;, the client adds &lt;code&gt;Mcp-Param-Region: eu-west1&lt;/code&gt; to the request.&lt;/p&gt;

&lt;p&gt;This is worth knowing even if you never use it, because it is the one part of a tool definition that can make a client discard your tool entirely. Over Streamable HTTP, a client must reject any tool whose &lt;code&gt;x-mcp-header&lt;/code&gt; value breaks the rules, and rejection means excluding that tool from the listing. Other tools on the same server keep working, so a single malformed definition shows up as one capability quietly missing rather than as an obvious failure. Clients are expected to log a warning naming the tool and the reason, which is usually where you will find the problem.&lt;/p&gt;

&lt;p&gt;The constraints are specific:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the value must not be empty, and must be a valid HTTP field-name token per RFC 9110;&lt;/li&gt;
&lt;li&gt;it must contain no control characters, including CR and LF;&lt;/li&gt;
&lt;li&gt;it must be unique, case-insensitively, among all &lt;code&gt;x-mcp-header&lt;/code&gt; values in that &lt;code&gt;inputSchema&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;it may only be applied to integer, string, or boolean properties — &lt;code&gt;number&lt;/code&gt; is not permitted, and integers must stay inside the IEEE 754 double-precision safe range;&lt;/li&gt;
&lt;li&gt;the property must be statically reachable from the schema root.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is also a judgment call the schema cannot make for you. Header values are visible to every network intermediary on the path, so passwords, API keys, tokens, and personal data should never be mirrored. Route on coarse, non-sensitive dimensions such as region, tenant, or workload class.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design state handles as ordinary arguments
&lt;/h2&gt;

&lt;p&gt;Because the protocol no longer keeps a session, a server cannot rely on implicit per-connection state to relate one call to the next. The specification's non-normative guidance is to make state explicit: a creation tool returns a handle, and later tools accept that handle as a normal argument. From the wire's point of view it is just a string.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"add_item"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Add a product to an existing basket. Create one with create_basket first."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inputSchema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"basket_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Handle returned by create_basket. Baskets expire after 24 hours of inactivity."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"pattern"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^bsk_[a-zA-Z0-9]{8,}$"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sku"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Product SKU to add."&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"basket_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sku"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"additionalProperties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four design points follow directly from that pattern, and three of them are schema or description decisions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authorization is not implied by possession.&lt;/strong&gt; On an authenticated server a handle is a name, not a capability, and the handler must check the caller's authorization against it on every call. On an unauthenticated server the handle is unavoidably a bearer token, so generate it with real entropy and give it a bounded lifetime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefer opaque handles.&lt;/strong&gt; Identifiers that encode internal structure invite parsing and guessing. The &lt;code&gt;pattern&lt;/code&gt; above constrains the shape enough to catch a mangled value without advertising what is inside.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State the lifetime where the model can see it.&lt;/strong&gt; Handles outlive any single connection, so the retention policy belongs in the creation tool's description. A model deciding whether to create state should be able to read how long that state survives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make expiry recoverable.&lt;/strong&gt; A call against an unknown or expired handle should come back as a tool execution error that says so, not as a protocol error. That is the difference between a model that creates a fresh basket and one that gives up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat annotations as hints, not permissions
&lt;/h2&gt;

&lt;p&gt;MCP supports annotations describing tool behavior. The specification requires clients to treat them as untrusted unless they come from a trusted server.&lt;/p&gt;

&lt;p&gt;An annotation saying that a tool is read-only does not replace authorization, user confirmation, access controls, or handler-side enforcement. Tool metadata describes intended behavior; the implementation must enforce actual behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate in layers
&lt;/h2&gt;

&lt;p&gt;No single green check proves that an MCP tool is production-ready. Use several layers.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Parse the JSON
&lt;/h3&gt;

&lt;p&gt;Reject malformed JSON before checking MCP fields. This catches missing commas, invalid quotes, comments, and trailing commas.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Check the MCP tool structure
&lt;/h3&gt;

&lt;p&gt;Verify the root object, name, description type, &lt;code&gt;inputSchema&lt;/code&gt;, &lt;code&gt;required&lt;/code&gt;, &lt;code&gt;properties&lt;/code&gt;, and optional containers. The &lt;a href="https://trexmi.com/tools/mcp-schema-validator/" rel="noopener noreferrer"&gt;MCP Schema Validator&lt;/a&gt; performs this focused structural pass and returns path-based errors plus compatibility warnings. It is free and runs locally in the browser, so an unpublished tool definition never leaves your machine.&lt;/p&gt;

&lt;p&gt;It deliberately does not claim to evaluate every JSON Schema keyword, resolve every reference, connect to a server, or call the handler.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Evaluate the full JSON Schema
&lt;/h3&gt;

&lt;p&gt;Use a Draft 2020-12-compatible validator to test realistic valid and invalid argument objects. Include boundary values, missing required fields, extra fields, empty strings, Unicode, large arrays, and nested failures.&lt;/p&gt;

&lt;p&gt;If your schema uses &lt;code&gt;$ref&lt;/code&gt;, check how your validator resolves references and confirm it matches what MCP clients are expected to do — reference resolution is called out separately in the security guidance precisely because a schema that resolves differently on each side is a schema that validates differently on each side.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Test the actual MCP integration
&lt;/h3&gt;

&lt;p&gt;List the tool through the production server and client, then call it with accepted and rejected inputs. Confirm authentication, authorization, user confirmation, timeouts, rate limits, error responses, and result validation. If you used &lt;code&gt;x-mcp-header&lt;/code&gt;, confirm the tool actually appears in &lt;code&gt;tools/list&lt;/code&gt; over Streamable HTTP rather than being silently dropped.&lt;/p&gt;

&lt;p&gt;The MCP specification requires servers to validate tool inputs, implement access controls, rate-limit invocations, and sanitize outputs. Clients should show sensitive inputs before sending them, validate results, apply timeouts, and keep appropriate audit records.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common failure patterns
&lt;/h2&gt;

&lt;p&gt;One frequent mistake is copying every key from one sample into &lt;code&gt;required&lt;/code&gt;. This turns convenient options into mandatory arguments and forces the model to invent values. Decide required fields from the operation's real minimum input, not from the example.&lt;/p&gt;

&lt;p&gt;Another mistake is using schema descriptions as decoration. A property description should clarify units, identifiers, accepted sources, or consequences that the type alone cannot express. "The limit" adds little; "Maximum number of passages to return, from 1 to 20" helps both developers and models.&lt;/p&gt;

&lt;p&gt;A newer failure mode is a tool that disappears without an error. If a client silently lacks one capability while the rest of the server works, check &lt;code&gt;x-mcp-header&lt;/code&gt; values before anything else — a duplicate name, a control character, or a &lt;code&gt;number&lt;/code&gt;-typed property is enough for a conforming client to drop that tool from the listing.&lt;/p&gt;

&lt;p&gt;Do not assume a structurally valid tool is safe to expose. A &lt;code&gt;delete_record&lt;/code&gt; schema may be perfectly formed while its handler lacks authorization or confirmation. Likewise, &lt;code&gt;additionalProperties: false&lt;/code&gt; blocks unknown keys but does not protect against malicious content inside an allowed string.&lt;/p&gt;

&lt;p&gt;Finally, avoid testing only the happy path. Calls fail because of expired credentials, inaccessible resources, timeouts, rate limits, conflicts, and invalid state. Return actionable tool execution errors where the model can correct the request, while keeping protocol and server failures distinct.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical release checklist
&lt;/h2&gt;

&lt;p&gt;Before publishing a tool, confirm that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the name is stable, specific, and unique within the server, and reads sensibly if a proxy prefixes it;&lt;/li&gt;
&lt;li&gt;the description explains the action, result, and selection boundary;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;inputSchema&lt;/code&gt; is a valid JSON Schema object with an object root;&lt;/li&gt;
&lt;li&gt;only genuinely mandatory properties appear in &lt;code&gt;required&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;strings, numbers, and arrays have realistic limits;&lt;/li&gt;
&lt;li&gt;nested objects and array items were reviewed manually;&lt;/li&gt;
&lt;li&gt;undeclared properties are either rejected or intentionally supported;&lt;/li&gt;
&lt;li&gt;any &lt;code&gt;x-mcp-header&lt;/code&gt; values are unique, token-safe, primitive-typed, and free of sensitive data;&lt;/li&gt;
&lt;li&gt;state handles declare their lifetime in the description and are re-authorized on every call;&lt;/li&gt;
&lt;li&gt;optional &lt;code&gt;outputSchema&lt;/code&gt; matches every structured result path, including array roots;&lt;/li&gt;
&lt;li&gt;structured results are also serialized into a text block for older clients;&lt;/li&gt;
&lt;li&gt;annotations are never used as a security boundary;&lt;/li&gt;
&lt;li&gt;valid, invalid, boundary, authorization, and failure calls are tested;&lt;/li&gt;
&lt;li&gt;the handler validates inputs again and sanitizes outputs;&lt;/li&gt;
&lt;li&gt;sensitive operations remain visible and confirmable by the user.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final takeaway
&lt;/h2&gt;

&lt;p&gt;A useful MCP tool contract combines precise metadata, a deliberately reviewed JSON Schema, and behavior tested through the real server and client. Generate the repetitive starting structure, then spend human attention on the parts a sample cannot infer: optionality, constraints, semantics, side effects, permissions, and failure behavior.&lt;/p&gt;

&lt;p&gt;That is the difference between JSON that looks like an MCP tool and a contract that clients can safely rely on.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/specification/2026-07-28/server/tools" rel="noopener noreferrer"&gt;MCP Tools specification 2026-07-28&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http" rel="noopener noreferrer"&gt;Streamable HTTP transport: custom headers from tool parameters&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://json-schema.org/draft/2020-12/json-schema-core" rel="noopener noreferrer"&gt;JSON Schema Draft 2020-12 Core&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://json-schema.org/draft/2020-12/json-schema-validation" rel="noopener noreferrer"&gt;JSON Schema Draft 2020-12 Validation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>jsonschema</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
