<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[YongBo Yu]]></title><description><![CDATA[YongBo Yu]]></description><link>https://yongboyu.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>YongBo Yu</title><link>https://yongboyu.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 18 Sep 2026 23:12:57 GMT</lastBuildDate><atom:link href="https://yongboyu.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Don't let agents invent your site: fetch, cache, and cite with a stdio MCP server]]></title><description><![CDATA[Ask an agent what a public site says about a product page, a topic note, or an essay. You usually get fluent prose: a stack list, a couple of metrics, a confident "this project uses X." What you rarel]]></description><link>https://yongboyu.hashnode.dev/don-t-let-agents-invent-your-site-fetch-cache-and-cite-with-a-stdio-mcp-server</link><guid isPermaLink="true">https://yongboyu.hashnode.dev/don-t-let-agents-invent-your-site-fetch-cache-and-cite-with-a-stdio-mcp-server</guid><category><![CDATA[mcp]]></category><category><![CDATA[Python]]></category><category><![CDATA[agents]]></category><category><![CDATA[opensource]]></category><dc:creator><![CDATA[Yong Yu]]></dc:creator><pubDate>Wed, 16 Sep 2026 23:13:39 GMT</pubDate><content:encoded><![CDATA[<p>Ask an agent what a public site says about a product page, a topic note, or an essay. You usually get fluent prose: a stack list, a couple of metrics, a confident "this project uses X." What you rarely get is a URL you can open.</p>
<p>That is the failure mode. The model is not retrieving the site. It is interpolating a site-shaped answer from training data, prior chat, and whatever fragments happened to be in the prompt. Project names drift. Metrics get rounded. Pages that never existed sound real.</p>
<p>The fix is not a better system prompt. It is a <strong>tool boundary</strong>: the client should only claim what a tool returned, and the tool should only return text it fetched, with the public URLs attached.</p>
<p><a href="https://github.com/YongBoYu1/site-context-mcp">site-context-mcp</a> is a small Python MCP server that does exactly that. It speaks MCP over <strong>stdio</strong> via FastMCP, fetches a public <code>llms.txt</code> plus the same-host pages it links, caches them in-process (~1 hour TTL), and exposes four tools. Every response is JSON text with a top-level <code>sources[]</code>.</p>
<p>This is <strong>site/project context</strong>, not an identity server. A sibling package, <a href="https://github.com/YongBoYu1/resume-mcp">resume-mcp</a>, already answers "who is this person" from <code>resume.json</code>. Mixing those jobs is how you get a resume tool that starts inventing product pages.</p>
<h2>The contract: no citation, no claim</h2>
<p>MCP is a typed tool protocol. A local process lists tools; the model calls them; the host returns structured results. The interesting part is what you put in those results.</p>
<p>This server treats <code>sources</code> as a required field, not a nicety. A helper merges the tool body with a de-duplicated URL list and <code>json.dumps</code> the whole thing. Error paths do the same: an empty search query, a missing page, a failed corpus load — all still include <code>sources</code>. If a fetch partially fails, the surviving pages are returned and the failures show up in <code>cache_notes</code>.</p>
<p>FastMCP <code>instructions</code> tell the host model to answer from the cached corpus and to cite those URLs. Instructions are not enforcement. The enforcement is the payload: if the snippet is not in the tool result, it did not come from this server.</p>
<h2>Seed from <code>llms.txt</code>, then same-host only</h2>
<p>On process start the server tries <code>cache.ensure_loaded()</code> (and again on first tool use if that failed, or when the in-process TTL expires — <code>CACHE_TTL_SECONDS = 3600</code>). The cache then:</p>
<ol>
<li><code>GET</code>s the wired host's <code>/llms.txt</code> (default: <code>https://yongbo-yu.vercel.app/llms.txt</code>)</li>
<li>Parses markdown links in document order</li>
<li>Fetches each allowed URL</li>
<li>Always seeds home, a known <code>/projects/...</code> evidence page, and <code>/resume.json</code> if parsing is thin</li>
</ol>
<p>Allowlisting is the security model. A URL is kept only if it is <strong>HTTPS</strong>, on the <strong>wired host</strong>, not a binary extension (images, fonts, PDFs, archives), and not a blocked fragment (off-host social, private admin hosts, internal app paths). Redirects that leave the host are dropped. There are no API keys and no cookies.</p>
<p>The default corpus is one public site. The shape — <code>llms.txt</code> as the machine-readable index, same-host follow, in-memory TTL — is the reusable part. Change the host constant to another public site that publishes <code>llms.txt</code> and you get the same tools.</p>
<p>HTML is stripped with a small <code>html.parser.HTMLParser</code>: skip <code>script</code> / <code>style</code> / <code>svg</code> / <code>noscript</code>, prefer <code>main</code> / <code>article</code> over the rest of <code>body</code>, take <code>&lt;title&gt;</code> (dropping a <code>| Site name</code> suffix) and meta / <code>og:description</code>. JSON pages are pretty-printed so keyword search can hit field values. Plain text (<code>llms.txt</code>) is stored as-is.</p>
<p>Fetch uses <code>httpx</code> (20s timeout, follow redirects, User-Agent <code>site-context-mcp/0.1.0</code>). A <code>threading.Lock</code> serializes reloads so two overlapping tool calls cannot double-fetch. Failures append to <code>cache.errors</code> instead of aborting the whole corpus — one 500 should not erase ten good pages.</p>
<h2>Four tools, one response shape</h2>
<table>
<thead>
<tr>
<th>Tool</th>
<th>What it returns</th>
</tr>
</thead>
<tbody><tr>
<td><code>list_site_pages</code></td>
<td>Catalog of cached pages: path, title, URL, kind, optional project slug</td>
</tr>
<tr>
<td><code>get_page</code></td>
<td>One page by path (<code>/essays/...</code>) or absolute HTTPS URL, plus extracted text</td>
</tr>
<tr>
<td><code>search_site</code></td>
<td>Keyword matches: score, snippet (≤400 chars), source URL, path, title</td>
</tr>
<tr>
<td><code>get_project</code></td>
<td>A <code>/projects/{slug}</code> evidence page; optional light fields from <code>resume.json</code></td>
</tr>
</tbody></table>
<p><code>search_site</code> is not embeddings. It lowercases the query, splits on whitespace, scores chunks by how many tokens appear, and keeps the best snippet per page plus title/description hits. HTML is split on sentences; <code>llms.txt</code> / JSON use paragraph blocks. <code>limit</code> is clamped to 1–25 (default 8). Dedup key is <code>source|snippet[:120]</code>.</p>
<p><code>get_project</code> is still site context. It resolves a slug or display name to a cached <code>/projects/...</code> page. If <code>resume.json</code> was fetched, a short cross-link (name, description, up to five highlights, dates) is attached and <code>resume.json</code> is added to <code>sources</code>. It does <strong>not</strong> grow identity or resume-summary tools.</p>
<p><code>list_site_pages</code> always puts <code>llms.txt</code> first in <code>sources</code>, then every page URL. <code>get_page</code> cites the page plus <code>llms.txt</code>. A miss returns <code>available_paths</code> so the model can recover without guessing.</p>
<p>Example — <code>get_page("/projects/kilodock")</code>. That path is one page in the default corpus, not the subject of this post. The tool returns JSON text shaped like:</p>
<pre><code class="language-json">{
  "url": "https://yongbo-yu.vercel.app/projects/kilodock",
  "path": "/projects/kilodock",
  "title": "KiloDock — Gym Operating System by YongBo Yu",
  "description": null,
  "kind": "html",
  "content_type": "text/html",
  "text": "KiloDock is a multi-tenant CrossFit gym operating system independently built by YongBo Yu. ... Mobile schedule latency: 2.84.6s down to 683ms. Admin load: six API calls / 3.05s down to one call / 778ms. ...",
  "cache_notes": null,
  "sources": [
    "https://yongbo-yu.vercel.app/projects/kilodock",
    "https://yongbo-yu.vercel.app/llms.txt"
  ]
}
</code></pre>
<p><code>search_site("LangGraph")</code> is the more typical agent call. Each match has <code>snippet</code> + <code>source</code>; the top-level <code>sources</code> array is the union of <code>llms.txt</code> and every hit URL. The client can quote a snippet and the URL in the same breath. If <code>cache_notes</code> is a non-null list, a page was skipped — treat the corpus as partial, not complete.</p>
<h2>Install and wire it into Cursor</h2>
<p>Python 3.11+. Dependencies: <code>mcp&gt;=1.9,&lt;2</code> and <code>httpx</code>. From a clone of the repo:</p>
<pre><code class="language-bash">pip install -e ".[dev]"
pytest -q
python -m site_context_mcp
</code></pre>
<p>Tests mock <code>httpx.Client</code>; they do not need live network. CI runs pytest on 3.11 and 3.12. After install, the console script is <code>site-context-mcp</code>. Equivalent: <code>uv run python -m site_context_mcp</code>.</p>
<p>The process speaks MCP on <strong>stdin/stdout</strong>. Do not print logs to stdout; they corrupt the protocol. FastMCP's default transport here is <code>stdio</code> — that is what Cursor and Claude Desktop spawn as a child process.</p>
<p>Cursor (<code>~/.cursor/mcp.json</code> or project <code>.cursor/mcp.json</code>):</p>
<pre><code class="language-json">{
  "mcpServers": {
    "site-context-mcp": {
      "command": "python3",
      "args": ["-m", "site_context_mcp"],
      "cwd": "/absolute/path/to/site-context-mcp"
    }
  }
}
</code></pre>
<p>If the package lives in a venv, point <code>command</code> at that interpreter. With uv:</p>
<pre><code class="language-json">{
  "mcpServers": {
    "site-context-mcp": {
      "command": "uv",
      "args": ["run", "python", "-m", "site_context_mcp"],
      "cwd": "/absolute/path/to/site-context-mcp"
    }
  }
}
</code></pre>
<p>Reload MCP servers. The four tools should appear under <code>site-context-mcp</code>. Then ask the client to list cached pages and cite the URLs, or to search the corpus and return snippets with sources. If the answer has no URL, it did not come from this tool.</p>
<h2>Why this is enough (and what it is not)</h2>
<p>This is not a crawler, not a vector store, and not a general web browser. It will not follow off-host links, will not ingest PDFs or images, and will not refresh more often than the TTL unless you restart the process (or wait out the hour). For a personal or project site that already publishes <code>llms.txt</code>, that is the right size: the index is the author's, the pages are public HTTPS, and the agent cannot silently mix in a different origin.</p>
<p>The alternative — stuffing the whole site into the system prompt — expires the moment a page changes and burns context on every turn. Caching in the MCP process keeps the corpus off the prompt until a tool call needs it. <code>sources[]</code> makes the citation path mechanical instead of a polite request in the system prompt.</p>
<p>If you already run <code>resume-mcp</code> for identity, run this beside it. One server for who someone is; one for what the site actually says. Keep the tool names honest, keep stdout clean, and refuse to answer a project claim that did not arrive with a URL.</p>
<hr />
<p>YongBo Yu (also Yong Yu) · Toronto · <a href="https://yongbo-yu.vercel.app">yongbo-yu.vercel.app</a> · <a href="https://github.com/YongBoYu1/site-context-mcp">github.com/YongBoYu1/site-context-mcp</a></p>
]]></content:encoded></item><item><title><![CDATA[Build a Runnable MCP Loop in Python (stdio → streamable-http → LLM tool choice)]]></title><description><![CDATA[Attributed Chinese → English compileSource: [MCP][02]快速入门MCP开发Original author: 花酒锄作田 (Cnblogs) · Posted: 2025-09-15This is an English rewrite of the original tutorial’s ideas and code patterns. It is ]]></description><link>https://yongboyu.hashnode.dev/build-a-runnable-mcp-loop-in-python-stdio-streamable-http-llm-tool-choice</link><guid isPermaLink="true">https://yongboyu.hashnode.dev/build-a-runnable-mcp-loop-in-python-stdio-streamable-http-llm-tool-choice</guid><category><![CDATA[mcp]]></category><category><![CDATA[Python]]></category><category><![CDATA[llm]]></category><category><![CDATA[AI]]></category><dc:creator><![CDATA[Yong Yu]]></dc:creator><pubDate>Wed, 16 Sep 2026 23:09:17 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p><strong>Attributed Chinese → English compile</strong><br /><strong>Source:</strong> <a href="https://www.cnblogs.com/XY-Heruo/p/19092074">[MCP][02]快速入门MCP开发</a><br /><strong>Original author:</strong> 花酒锄作田 (Cnblogs) · <strong>Posted:</strong> 2025-09-15<br />This is an English rewrite of the original tutorial’s ideas and code patterns. It is <strong>not</strong> original work by the compiler. Always link the Chinese source; do not present this compile as the original.</p>
</blockquote>
<hr />
<p>Many MCP write-ups only show how to register a Server and paste it into Cursor. The Cnblogs post by 花锄作田 is useful for product engineers because it also builds the <strong>Client / Host side</strong>: list prompts, resources, and tools; call them over stdio; switch to streamable HTTP; then let an LLM decide which tool to invoke.</p>
<p>If you are shipping agents into a backend, that Client loop is the missing middle between “SDK demo and “our service owns the tool session.” You need a reliable discover → bind → call → feed-back loop before you care which model sits on top.</p>
<h2>Environment</h2>
<p>The author used Python <strong>3.13.5</strong> (3.11+ is fine). Prefer <code>uv</code> or pip:</p>
<pre><code class="language-bash"># uv
uv add mcp fastmcp

# or pip
python -m pip install mcp fastmcp
</code></pre>
<p>Notes from the source:</p>
<ul>
<li>The official <code>mcp</code> package ships FastMCP v1; community FastMCP has moved to v2—trying both while learning is fine.  </li>
<li>Write <strong>type hints, return types, and docstrings</strong> carefully. Those become the model-facing tool descriptions later.</li>
</ul>
<h2>Step 1 — Minimal FastMCP Server (stdio)</h2>
<p>Concept: prompts, resources, and tools on one server with <code>transport="stdio"</code>.</p>
<p>Illustrative shape (adapted from the original). <strong>Trim any SSH / remote-shell tools</strong> before you run this locally unless you harden them first:</p>
<pre><code class="language-python">from datetime import datetime
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("custom")

@mcp.prompt()
def greet_user(name: str, style: str = "formal") -&gt; str:
    """Greet a user with a specified style."""
    if style == "friendly":
        return f"Hey {name}! What's up?"
    return f"Hello, {name}!"

@mcp.resource("greeting://{name}")
def greeting_resource(name: str) -&gt; str:
    """A simple greeting resource."""
    return f"Hello, {name}!"

@mcp.resource("config://app")
def get_config() -&gt; str:
    """Static configuration data"""
    return "App configuration here"

@mcp.tool()
def add(a: int, b: int) -&gt; int:
    """Add two numbers"""
    return a + b

@mcp.tool()
async def get_date() -&gt; str:
    """Get today's date."""
    return datetime.now().strftime("%Y-%m-%d")

@mcp.tool()
async def get_weather(city: str) -&gt; str:
    """Get weather for a given city."""
    return f"It's always sunny in {city}!"

if __name__ == "__main__":
    mcp.run(transport="stdio")
</code></pre>
<p><strong>Preflight:</strong> run the server script alone once. If imports fail, the Client will fail in a confusing way when it tries to spawn the child process.</p>
<blockquote>
<p><strong>Production caution — SSH / god-mode shell:</strong> the original also demonstrates a remote SSH tool. Treat that as <strong>high-risk</strong>. Do not expose arbitrary remote command execution to a model without allowlists, authentication, and human confirmation. Prefer narrow, typed tools over “run anything on this host.”</p>
</blockquote>
<h2>Step 2 — Stdio Client with <code>ClientSession</code></h2>
<p>The Client launches the Server as a subprocess via <code>StdioServerParameters</code> (absolute interpreter, script path, and cwd). Pattern from the source:</p>
<pre><code class="language-python">import asyncio
from pathlib import Path
from pydantic import AnyUrl

from mcp import ClientSession, StdioServerParameters, types
from mcp.client.stdio import stdio_client

server_params = StdioServerParameters(
    command=str(Path(__file__).parent / ".venv" / "bin" / "python"),
    args=[str(Path(__file__).parent / "demo1-server.py")],
    cwd=str(Path(__file__).parent),
)

async def run():
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()

            prompts = await session.list_prompts()
            print([p.name for p in prompts.prompts])

            tools = await session.list_tools()
            print([t.name for t in tools.tools])

            resource_content = await session.read_resource(AnyUrl("greeting://World"))
            block = resource_content.contents[0]
            if isinstance(block, types.TextResourceContents):
                print(block.text)

            result = await session.call_tool("add", arguments={"a": 5, "b": 3})
            print(result.content[0].text if result.content else result)
            print(result.structuredContent)

if __name__ == "__main__":
    asyncio.run(run())
</code></pre>
<p>Expected behavior (as reported in the original run): prompts listed, resource text returned, <code>add</code> yields <code>8</code> plus structured content.</p>
<p><strong>Failure mode to remember:</strong> starting the Client also starts the Server. Server syntax or import errors look like Client connection failures—debug the Server first.</p>
<h2>Step 3 — Same Server over streamable-http</h2>
<p>Server change:</p>
<pre><code class="language-python">mcp = FastMCP("custom", host="localhost", port=8001)

if __name__ == "__main__":
    mcp.run(transport="streamable-http")
</code></pre>
<p>Client change (conceptually): use <code>streamablehttp_client("http://localhost:8001/mcp")</code>, then the same <code>ClientSession.initialize()</code> / <code>list_*</code> / <code>call_tool</code> flow. The source notes a third return value, <code>get_session_id</code>, for session managementusually unused in hello-worlds.</p>
<p>This is the fork most product backends care about: <strong>stdio for desktop or host-local tools</strong>, <strong>HTTP for remotely deployed tool servers</strong>.</p>
<p>Docs and ecosystem starting points:</p>
<ul>
<li><a href="https://modelcontextprotocol.io/">MCP Python SDK / quickstart</a>  </li>
<li>FastMCP docs (community) for transport options</li>
</ul>
<h2>Step 4 — Let the LLM choose tools</h2>
<p>Server stays the same. Client:</p>
<ol>
<li>Connect (HTTP example in the post).  </li>
<li>Call <code>list_tools()</code> and map each tool to an OpenAI-compatible function schema (<code>name</code>, <code>description</code>, <code>parameters</code> from <code>inputSchema</code>).  </li>
<li>Call chat completions with <code>tools=...</code>.  </li>
<li>While the model emits tool calls: <code>session.call_tool(name, args)</code>, append assistant and tool messages, call the model again.  </li>
<li>Stop when there are no more tool calls.</li>
</ol>
<p>The original uses an OpenAI-compatible client pointed at <strong>Qwen / DashScope</strong> (<code>qwen-plus</code>, <code>compatible-mode/v1</code>). Any OpenAI-tools-compatible endpoint works the same way (DeepSeek, OpenAI, and similar).</p>
<p>Config sketch from the source’s supplementary modules:</p>
<pre><code class="language-json">{
  "llm": {
    "model": "qwen-plus",
    "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
    "api_key": "your token"
  },
  "server": {
    "host": "127.0.0.1",
    "port": 8000
  }
}
</code></pre>
<p>Example interactive outcomes from the original session:</p>
<ul>
<li>“What is todays date?” → <code>get_date</code>  </li>
<li>“Weather in Hefei?” → <code>get_weather</code> with <code>{city: ""}</code>  </li>
<li>Numeric compare → custom comparison tool</li>
</ul>
<p>That is the whole product loop in miniature: <strong>discover → bind schemas  model proposes → your code executes  feed results back</strong>.</p>
<h2>Logging pitfall while integrating the LLM</h2>
<p>The author’s sample logger can write to a file; if you stream-log, keep protocol traffic on the MCP pipes and human logs elsewhere. Mixing debug prints into a stdio Server’s <strong>stdout</strong> will break JSON-RPC—the same lesson every serious MCP guide repeats.</p>
<h2>Why this matters for agents / MCP / RAG products</h2>
<p>Shipping an LLM feature is less about a single chat completion and more about a <strong>reliable tool session</strong>: spawn or connect to servers, refresh schemas, bound the agent loop, and keep transports swappable (local stdio versus remote HTTP). The same Client you use for MCP tools is where you later hang RAG retrieval as a resource or tool—without rewriting the host when you add the next capability. Get this loop solid once, and every new tool becomes a schema change instead of a host rewrite.</p>
<hr />
<h3>Compiler (not original author)</h3>
<p>English compile by YongBo Yu.<br /><a href="https://yongbo-yu.vercel.app">https://yongbo-yu.vercel.app</a> · <a href="https://github.com/YongBoYu1">https://github.com/YongBoYu1</a>  </p>
<p>Original Chinese article © 花酒锄作田 / Cnblogs. Always link the source; do not present this compile as the original.</p>
]]></content:encoded></item><item><title><![CDATA[Treat Your MCP Server as a Tool Boundary (stdio, schemas, and safe local calls)]]></title><description><![CDATA[Attributed Chinese → English compileSource: MCP Server 实战：从协议到本地工具调用Original author: AJie (AJie's Blog) · Published: 2026-06-03This is a faithful English rewrite for learning. It is not original resea]]></description><link>https://yongboyu.hashnode.dev/treat-your-mcp-server-as-a-tool-boundary-stdio-schemas-and-safe-local-calls</link><guid isPermaLink="true">https://yongboyu.hashnode.dev/treat-your-mcp-server-as-a-tool-boundary-stdio-schemas-and-safe-local-calls</guid><category><![CDATA[mcp]]></category><category><![CDATA[AI]]></category><category><![CDATA[agents]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[Yong Yu]]></dc:creator><pubDate>Wed, 16 Sep 2026 23:02:23 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p><strong>Attributed Chinese → English compile</strong><br /><strong>Source:</strong> <a href="https://www.promptnet.cn/2026/06/03/mcp-server-local-tool-calling-practice/">MCP Server 实战：从协议到本地工具调用</a><br /><strong>Original author:</strong> AJie (AJie's Blog) · <strong>Published:</strong> 2026-06-03<br />This is a faithful English rewrite for learning. It is <strong>not</strong> original research by the compiler. Always link the Chinese original; do not present this compile as the source work.</p>
</blockquote>
<hr />
<p>Most first MCP tutorials jump straight to “register a tool, wire stdio, celebrate the demo.” AJie’s post is useful because it starts one layer earlier: <strong>an MCP Server is a capability boundary</strong>, not a dumping ground for whatever the model might want to run on your machine.</p>
<table>
<thead>
<tr>
<th>Dimension</th>
<th>Healthy MCP tool</th>
<th>Fragile MCP tool</th>
</tr>
</thead>
<tbody><tr>
<td>Input</td>
<td>Explicit fields, types, constraints</td>
<td>Free-form natural language for the tool to “figure out”</td>
</tr>
<tr>
<td>Output</td>
<td>Stable structure the model can continue from</td>
<td>Raw logs / untyped blobs</td>
</tr>
<tr>
<td>Scope</td>
<td>One action (or one family)</td>
<td>“Do anything” shell god-mode</td>
</tr>
<tr>
<td>Failure</td>
<td>Explainable error the model can act on</td>
<td>Opaque stack traces</td>
</tr>
<tr>
<td>Safety</td>
<td>Path / command / network limits</td>
<td>Default-open to the whole host</td>
</tr>
</tbody></table>
<p>AJie’s practical rule: if the model must <strong>understand intent</strong> and the tool should do <strong>deterministic work</strong>, make an MCP tool. If the work itself still needs open-ended judgment (“rewrite this article”), leave that to the model and only give it evidence tools.</p>
<h2>Scenario: project analysis without “give me shell”</h2>
<p>Instead of exposing a generic shell, split a local project-assistant into narrow tools:</p>
<table>
<thead>
<tr>
<th>Tool</th>
<th>Input</th>
<th>Output</th>
<th>Why</th>
</tr>
</thead>
<tbody><tr>
<td><code>list_project_files</code></td>
<td>root, Less flexible than bash. Much more operable in production: no accidental deletes, and outputs stay consumable across turns.</td>
<td></td>
<td></td>
</tr>
</tbody></table>
<h2>Design I/O so the model guesses less</h2>
<p>Tool descriptions are <strong>routing context for the model</strong>, not human README fluff.</p>
<p>Weak:</p>
<blockquote>
<p>Read a file from local project.</p>
</blockquote>
<p>Stronger (paraphrased from the source’s guidance):</p>
<blockquote>
<p>Read a text file under the configured project root. Use when you need a specific source, markdown, or config file. Path must be relative to project root. Optional <code>start</code> / <code>limit</code> restrict the returned line range.</p>
</blockquote>
<p>Suggested parameters for <code>read_project_file</code>:</p>
<ul>
<li><code>path</code> (string): relative to project root  </li>
<li><code>start</code> (number): start line  </li>
<li><code>limit</code> (number): max lines</li>
</ul>
<p>Suggested structured return:</p>
<ol>
<li>The user configures a launch command in the host (Claude Desktop, Claude Code, Cursor, or your own host).  </li>
<li>The client starts a local server process.  </li>
<li>The server waits on stdio for initialize.  </li>
<li>The client lists tools.  </li>
<li>The model decides whether to call.  </li>
<li>The client forwards <code>tools/call</code>.  </li>
<li>The server runs the local capability and returns JSON-RPC over stdout.</li>
</ol>
<h3>Troubleshooting map (from the source)</h3>
<table>
<thead>
<tr>
<th>Symptom</th>
<th>Likely cause</th>
<th>First check</th>
</tr>
</thead>
<tbody><tr>
<td>Client sees no tools</td>
<td>Process fail / bad path / init fail</td>
<td>Run the launch command alone</td>
</tr>
<tr>
<td>Tools listed, call fails</td>
<td>Schema mismatch / tool exception</td>
<td>Log args + structured error</td>
</tr>
<tr>
<td>Hang after call</td>
<td>No response / stdout polluted by logs</td>
<td>Separate stdout vs stderr</td>
</tr>
<tr>
<td>Works in terminal, fails in host</td>
<td>Env / cwd / permissions differ</td>
<td>Diff the client launch environment</td>
</tr>
<tr>
<td>Flaky</td>
<td>External cmds / file state</td>
<td>Boundary checks + clear errors</td>
</tr>
</tbody></table>
<p><strong>Hard rule for stdio:</strong> never write casual debug logs to <strong>stdout</strong>. Stdout is the protocol channel. Use <strong>stderr</strong> or a log file.
AJie recommends a tiny <code>ping_project</code> tool first: accept a string; return project name, cwd, and the echoed input. You are not proving business value—you are proving:</p>
<ul>
<li>The client can spawn the server  </li>
<li>Initialize completes  </li>
<li>The tool appears in the list  </li>
<li>Args round-trip  </li>
<li>The response renders in the host  </li>
<li>stderr does not corrupt the protocol</li>
</ul>
<p>Then add <strong>one real tool at a time</strong> and call it from the host after each addition. When something breaks, the last delta is obvious.</p>
<p>Many “works locally, fails in Claude Code” issues are path, env, or cwd mismatches—not SDK bugs.</p>
<h2>Safety tiers before you expose writes</h2>
<table>
<thead>
<tr>
<th>Tier</th>
<th>Examples</th>
<th>Default policy</th>
</tr>
</thead>
<tbody><tr>
<td>Read-only</td>
<td>List dir, read snippet, status query</td>
<td>Ship first</td>
</tr>
<tr>
<td>Constrained write</td>
<td>Drafts / reports in a sandbox dir</td>
<td>Allowlisted paths + types</td>
</tr>
<tr>
<td>High risk</td>
<td>Delete, arbitrary shell, push, publish</td>
<td>Off by default or human confirm</td>
</tr>
</tbody></table>
<p>Do not accept arbitrary absolute paths. Do not ship “run any shell command” as a general tool. Prefer allowlisted roots such as <code>reports/</code> or <code>drafts/</code>.
Example failure shape (paraphrased): instead of bare <code>ENOENT</code>, return <code>file_not_found</code> plus a hint to call <code>list_project_files</code> first. That turns a retry loop into a recovery path.</p>
<h2>Rollout checklist for a real project</h2>
<ol>
<li>Pick one narrow scenario (one sentence).  </li>
<li>Split two or three <strong>read-only</strong> tools with clear I/O.  </li>
<li>Prove the stdio minimum loop in the target host.  </li>
<li>Connect real data with truncated, structured returns.  </li>
<li>Add recoverable error objects.  </li>
<li>Guard context budget (paginate or summarize).  </li>
<li>Only then consider writes with directory and confirmation gates.</li>
</ol>
<h2>Why this matters if you are shipping agents / MCP into a product</h2>
<p>Product agents fail less often on “model IQ” than on <strong>tool surface area</strong>: over-broad tools, polluted stdio, and unrecoverable errors. MCP gives you a standard discovery and call pipe; you still have to design the boundary as carefully as a public API. Pair this with RAG the same way—retrieve evidence as structured resources, execute side effects as narrow tools, and keep both behind server-side policy. That is the difference between a demo that works once and a capability you can operate.</p>
<hr />
<h3>Compiler (not original author)</h3>
<p>English compile by YongBo Yu.<br />Site: <a href="https://yongbo-yu.vercel.app">https://yongbo-yu.vercel.app</a> · GitHub: <a href="https://github.com/YongBoYu1">https://github.com/YongBoYu1</a>  </p>
<p>Original post remains © AJie / source site. Read the Chinese original before publishing any derivative.</p>
<h2>Debug order that actually works</h2>
<ol>
<li>Run the server start command alone.  </li>
<li>Verify initialize with the ping tool.  </li>
<li>Capture model-supplied args versus the schema.  </li>
<li>Cap output length.  </li>
<li>Return structured errors (<code>ok</code>, <code>error</code>, <code>message</code>, <code>path</code>, <code>hint</code>).  </li>
<li>Only then attach real files, APIs, or commands.</li>
</ol>
<p>Official references the source points at:</p>
<ul>
<li><a href="https://modelcontextprotocol.io/">Model Context Protocol docs</a>  </li>
<li><a href="https://github.com/modelcontextprotocol/typescript-sdk">MCP TypeScript SDK</a>  </li>
<li>Host docs for your target client (Claude Code, Cursor, etc.)</li>
</ul>
<h2>Ship a minimum closed loop before real tools</h2>
<ul>
<li><code>path</code>, <code>start</code>, <code>end</code>, <code>content</code>, <code>truncated</code></li>
</ul>
<p>Truncation and pagination beat dumping tens of thousands of lines into the context window. MCP Servers should default to <strong>summaries, slices, or pages</strong>.</p>
<h2>The stdio call chain (local MCP’s default path)</h2>
<p>Local MCP almost always means:</p>
<p>file types | path list | Scope the workspace |
| <code>read_project_file</code> | relative path, line range | snippet | Pull only needed context |
| <code>summarize_markdown_posts</code> | directory, limit | title / date / summary table | Inventory content |
| <code>find_internal_links</code> | path | in-page links | Structure checks |
| <code>check_frontmatter</code> | path | missing / bad fields | Pre-publish lint |</p>
<p>If you are wiring agents, MCP, or RAG into a product, that framing matters. Models do not “use your laptop”; they generate call intents against schemas. Your job is to expose <strong>narrow, typed, recoverable</strong> actions—and keep stdout clean so the JSON-RPC pipe does not die under log noise. Over-broad tools and silent protocol corruption show up as “the agent is flaky,” when the real failure is the tool surface.</p>
<h2>What a good tool boundary looks like</h2>
]]></content:encoded></item></channel></rss>