---
title: "MCP Boundaries: Discovery, Identity, and Call Permissions"
description: "Examine Model Context Protocol boundaries: tool discovery, server instance tracking, and execution permissions in reviewable developer environments."
image: "https://zengliangyi.com/og/blog/en/mcp-protocol-boundaries.png"
---

MCP reduces repeated implementation of discovery and invocation across applications. It defines communication and capability descriptions; applications still own the knowledge base, permissions, and output meaning. The [minimal MCP server](/en/blog/mcp-server-implementation/) verifies a stdio handshake. This article examines instance identity and the difference between discovering a tool and permitting its invocation.

## Hosts, clients, and servers have separate responsibilities

The [2025-11-25 architecture](https://modelcontextprotocol.io/specification/2025-11-25/architecture) distinguishes a host, its clients, and servers. The host coordinates model interaction, connections, and call policy; a client communicates with one server; a server exposes capabilities such as tools. SDK 1.30.0 pins LATEST_PROTOCOL_VERSION to 2025-11-25, so this article cites that revision without calling it the latest specification.

A model tool call expresses a requested operation, and the application executes it. MCP makes discovery and invocation reusable across applications. Executors may reach remote services or run locally. Process location alone cannot distinguish model tool calling from MCP integration.

That revision defines stdio and Streamable HTTP as standard transports; see [transports](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports). Stdio requires no MCP HTTP listening port, while a handler may still use a network. Child-process permissions, connection options, service authentication, and business write checks remain relevant.

## ChatCrystal exposes seven tools backed by HTTP

[MCP server.ts](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/cli/mcp/server.ts) constructs CrystalClient, requests status, registers seven tools, and connects StdioServerTransport. Its version comes from the CLI package. Four knowledge reads are search_knowledge, get_note, list_notes, and get_relations; the others are recall_for_task, validate_task_memory, and write_task_memory. The names match the original, but its old hard-coded version and under-150-lines claim are not current evidence.

Handlers delegate to [CrystalClient](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/cli/client.ts) and serialize results as text. Semantic search, keyword-filtered lists, and task-memory recall are distinct entry points. A validation dry run does not replace the write operation’s business checks. The protocol layer does not directly open the knowledge-base database.

[mcp command](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/cli/commands/mcp.ts) uses [resolveConnection](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/cli/connection.ts) to resolve a base URL and token from the local default, explicit options, environment, or saved target. The same stdio interface can reach different HTTP-backed instances; configured loopback instance checks also differ from the implicit local default. A tool name, successful status, or returned sentence cannot alone establish the expected dataset. This article does not run crystal mcp or access a real knowledge base.

## Check boundaries with two in-memory servers

Install `npm install --ignore-scripts --save-exact @modelcontextprotocol/sdk@1.30.0 zod@4.6.2` in an independent directory. Save lesson.mjs and run Node 24+. Two real SDK clients and servers use InMemoryTransport, with temporary string state only. There is no stdio subprocess, HTTP, model, or actual ChatCrystal. allowedCall is a hand-written teaching host policy, rather than an AI host’s permission implementation.



```js
import   assert   from   'node:assert/strict'  ;  
  import   { Client }   from   '@modelcontextprotocol/sdk/client/index.js'  ;  
  import   { McpServer }   from   '@modelcontextprotocol/sdk/server/mcp.js'  ;  
  import   { InMemoryTransport }   from   '@modelcontextprotocol/sdk/inMemory.js'  ;  
  import   { z }   from   'zod'  ;  
  
  async   function   peer  (  instance  ) {  
    let   value   =   instance;  
    let   reads   =   0  ;  
    const   server   =   new   McpServer  ({ name:   'fixture-'   +   instance, version:   '1.0.0'   });  
    server.  registerTool  (  
      'read_fixture'  ,  
      {  
        description:   'Read one synthetic value in this instance.'  ,  
        inputSchema: { id: z.  number  () },  
        annotations: { readOnlyHint:   true   },  
      },  
      async   ()   =>   {  
        reads   +=   1  ;  
        return   { content: [{ type:   'text'  , text: value }] };  
      }  
    );  
    server.  registerTool  (  
      'write_fixture'  ,  
      {  
        description:   'Replace one temporary in-memory fixture value.'  ,  
        inputSchema: { value: z.  string  ().  min  (  1  ) },  
        annotations: { readOnlyHint:   false   },  
      },  
      async   (  input  )   =>   {  
        value   =   input.value;  
        return   { content: [{ type:   'text'  , text: value }] };  
      }  
    );  
    const   client   =   new   Client  ({ name:   'fixture-client'  , version:   '1.0.0'   });  
    const   [  clientTransport  ,   serverTransport  ]   =   InMemoryTransport.  createLinkedPair  ();  
    await   server.  connect  (serverTransport);  
    await   client.  connect  (clientTransport);  
    return   {  
      client,  
      server,  
      get   reads  () {  
        return   reads;  
      },  
    };  
  }  
  async   function   allowedCall  (  client  ,   name  ,   args  ,   allowed  ) {  
    if   (  !  allowed.  has  (name))   throw   new   Error  (  'blocked by teaching host policy'  );  
    return   client.  callTool  ({ name, arguments: args });  
  }  
  const   a   =   await   peer  (  'A'  );  
  const   b   =   await   peer  (  'B'  );  
  try   {  
    const   names   =   (  await   a.client.  listTools  ()).tools.  map  ((  tool  )   =>   tool.name).  sort  ();  
    assert.  deepEqual  (names, [  'read_fixture'  ,   'write_fixture'  ]);  
    assert.  deepEqual  ((  await   b.client.  listTools  ()).tools.  map  ((  tool  )   =>   tool.name).  sort  (), names);  
    const   allowed   =   new   Set  ([  'read_fixture'  ]);  
    const   first   =   await   allowedCall  (a.client,   'read_fixture'  , { id:   1   }, allowed);  
    const   second   =   await   allowedCall  (b.client,   'read_fixture'  , { id:   1   }, allowed);  
    assert.  equal  (first.content[  0  ].text,   'A'  );  
    assert.  equal  (second.content[  0  ].text,   'B'  );  
    const   before   =   a.reads;  
    const   bad   =   await   a.client.  callTool  ({ name:   'read_fixture'  , arguments: { id:   'one'   } });  
    assert.  equal  (bad.isError,   true  );  
    assert.  equal  (a.reads, before);  
    await   assert.  rejects  (  
      ()   =>   allowedCall  (a.client,   'write_fixture'  , { value:   'changed'   }, allowed),  
      /  blocked by teaching host policy  /  
    );  
    assert.  equal  (  
      (  await   a.client.  callTool  ({ name:   'read_fixture'  , arguments: { id:   1   } })).content[  0  ].text,  
      'A'  
    );  
    await   a.client.  callTool  ({ name:   'write_fixture'  , arguments: { value:   'changed'   } });  
    assert.  equal  (  
      (  await   a.client.  callTool  ({ name:   'read_fixture'  , arguments: { id:   1   } })).content[  0  ].text,  
      'changed'  
    );  
    console.  log  (  'instance identity, schema errors, and host policy boundaries pass'  );  
  }   finally   {  
    await   a.client.  close  ();  
    await   b.client.  close  ();  
    await   a.server.  close  ();  
    await   b.server.  close  ();  
  }
```



Both instances list the same names but return A and B. An invalid id produces isError without increasing the read handler count. The teaching policy blocks a write; a direct SDK call can still change the temporary value. Behavior annotations and schemas are not business authorization.

Change the second instance’s expected B to A and its assertion fails. Remove the allowlist check and the expected-rejection assertion fails. Restore each to pass. These checks establish the specified fixture, not universal host support for this SDK revision or policy.

## Acceptance continues after discovery

The [tool specification](https://modelcontextprotocol.io/specification/2025-11-25/server/tools) describes annotations as behavior metadata and requires care with untrusted sources. Even a tool marked read-only needs its actual handler, connection permissions, and returned data checked. Valid arguments do not establish relevant retrieval, accurate content, or an authorized write.

Verify initialization and version, discovery, target instance, schema rejection, authentication, business rejection, output, and shutdown separately. For writes, also check repeated calls and state after failure. MCP provides a reusable interface; reliability requires concrete evidence across these boundaries. Continue with [MCP setup](/en/blog/chatcrystal-mcp-setup/) when configuring a real connection.

Share:

##  Related Posts

[##  A Minimal MCP Server: Register Tools & Verify Stdio Calls

Build an offline addition tool with TypeScript SDK, verify protocol discovery and calls, and inspect ChatCrystal adapter integration boundaries.          曾亮懿       October 6, 2026      4 min read

#ChatCrystal

#MCP

#Node.js](/en/blog/mcp-server-implementation)    [##  AI Conversation Formats: JSONL Events vs SQLite Snapshots

Compare 5 AI adapter formats: separate JSONL events from SQLite keys, and inspect indexed, orphan, and unknown-version chat transcripts in memory stores.          曾亮懿       October 7, 2026      4 min read

#ChatCrystal

#Node.js

#SQLite](/en/blog/ai-conversation-format-divergence)    [##  How Claude Code, Cursor, Copilot, and Trae Store Chats on Disk

Compare Claude Code, Cursor, Copilot, and Trae storage paths. Distinguish raw copies from exports, and verify migration retention with test manifests.          曾亮懿       October 7, 2026      4 min read

#ChatCrystal

#Node.js

#SQLite](/en/blog/ai-conversation-management-options)

##  From writing to code

Explore ChatCrystal, developer tools and daily learning notes on my GitHub. Questions about the implementation are welcome.

[Visit my GitHub](https://github.com/ZengLiangYi)

```json
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "ZengLiangYi Blog",
  "alternateName": [
    "Zeng Liangyi",
    "ZengLiangYi",
    "zengliangyi.com"
  ],
  "url": "https://zengliangyi.com",
  "description": "曾亮懿的个人网站。记录 AI Native 探索、开发者工具与全栈产品实践。"
}
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "MCP Boundaries: Discovery, Identity, and Call Permissions",
  "description": "Examine Model Context Protocol boundaries: tool discovery, server instance tracking, and execution permissions in reviewable developer environments.",
  "url": "https://zengliangyi.com/en/blog/mcp-protocol-boundaries/",
  "image": "https://zengliangyi.com/images/blog/mcp-protocol-boundaries.svg",
  "datePublished": "2026-10-07T00:00:00.000Z",
  "dateModified": "2026-10-23T00:00:00.000Z",
  "author": {
    "@type": "Person",
    "name": "曾亮懿",
    "url": "https://zengliangyi.com/en/about"
  },
  "publisher": {
    "@type": "Organization",
    "name": "ZengLiangYi Blog",
    "logo": {
      "@type": "ImageObject",
      "url": "https://zengliangyi.com/favicon-96x96.png"
    }
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://zengliangyi.com/en/blog/mcp-protocol-boundaries/"
  }
}
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Home",
      "item": "https://zengliangyi.com/en"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "Blog",
      "item": "https://zengliangyi.com/en/blog"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "MCP Boundaries: Discovery, Identity, and Call Permissions",
      "item": "https://zengliangyi.com/en/blog/mcp-protocol-boundaries/"
    }
  ]
}
```
