{"id":106,"date":"2026-10-31T16:00:00","date_gmt":"2026-10-31T15:00:00","guid":{"rendered":"https:\/\/hoja.in\/blog\/a-tiny-mcp-server-that-exposes-read-only-project-checks\/"},"modified":"2026-10-16T14:00:12","modified_gmt":"2026-10-16T12:00:12","slug":"a-tiny-mcp-server-that-exposes-read-only-project-checks","status":"publish","type":"post","link":"https:\/\/hoja.in\/blog\/a-tiny-mcp-server-that-exposes-read-only-project-checks\/","title":{"rendered":"A tiny MCP server that exposes read-only project checks"},"content":{"rendered":"<p>When an AI agent works on a TYPO3 project, it mostly needs to answer three questions again and again: is the Composer setup still valid, does static analysis complain, and does the site still render? The easy way is to let the agent run shell commands. The problem with the easy way is that a shell can do everything else too.<\/p>\n<p>The Model Context Protocol (MCP) gives you another option: you write a small server that offers a few <em>tools<\/em> with typed inputs, and the agent can call exactly those. Nothing more. This post builds such a server with the official TypeScript SDK, with three read-only tools for a DDEV-based PHP project.<\/p>\n<h2>Why read-only tools are easier to trust<\/h2>\n<p>With a shell, every permission decision is about a string. Is <code>ddev exec composer validate<\/code> safe? Yes. Is <code>ddev exec composer validate &amp;&amp; rm -rf var\/<\/code> safe? Claude Code splits compound commands and checks each part, but you still review strings, and strings have many shapes.<\/p>\n<p>With an MCP tool, the decision is about a function you wrote. The <code>phpstan<\/code> tool below accepts a path from a fixed list and a level from 0 to 10. There is no way to pass it a second command, because the command is assembled inside the server with <code>execFile<\/code>, without a shell in between. You review the code once, and every call afterwards is boring. Boring is what I want from an agent&#8217;s tools.<\/p>\n<h2>Step 1: set up the project<\/h2>\n<p>The MCP quickstart currently uses the package <code>@modelcontextprotocol\/server<\/code> together with <code>zod<\/code> for input schemas, and Node.js 20 or newer. I put the server into <code>tools\/project-checks\/<\/code> of the project:<\/p>\n<pre><code class=\"language-bash\">mkdir -p tools\/project-checks\/src &amp;&amp; cd tools\/project-checks\nnpm init -y\nnpm install @modelcontextprotocol\/server zod\nnpm install -D @types\/node typescript<\/code><\/pre>\n<p>In <code>package.json<\/code> set <code>\"type\": \"module\"<\/code> and a <code>\"build\": \"tsc\"<\/code> script. The <code>tsconfig.json<\/code> from the quickstart works as is (<code>module<\/code> and <code>moduleResolution<\/code> set to <code>Node16<\/code>, <code>outDir<\/code> <code>.\/build<\/code>, <code>rootDir<\/code> <code>.\/src<\/code>).<\/p>\n<h2>Step 2: the server<\/h2>\n<pre><code class=\"language-typescript\">import { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\nimport { McpServer } from \"@modelcontextprotocol\/server\";\nimport { serveStdio } from \"@modelcontextprotocol\/server\/stdio\";\nimport { z } from \"zod\";\n\nconst run = promisify(execFile);\nconst PROJECT_DIR = process.env.PROJECT_DIR ?? process.cwd();\nconst BASE_URL = new URL(process.env.BASE_URL ?? \"https:\/\/myport.ddev.site\");\n\n\/\/ Runs a command inside the DDEV web container. No shell is involved.\nasync function ddev(args: string[], timeout = 120_000): Promise&lt;string&gt; {\n  try {\n    const { stdout, stderr } = await run(\"ddev\", [\"exec\", ...args], {\n      cwd: PROJECT_DIR,\n      timeout,\n      maxBuffer: 2 * 1024 * 1024,\n    });\n    return (stdout + stderr).trim() || \"(no output)\";\n  } catch (error: any) {\n    \/\/ A non-zero exit code is a result, not a crash: report the output\n    return `exit ${error.code ?? \"?\"}n${(error.stdout ?? \"\") + (error.stderr ?? \"\")}`.trim();\n  }\n}\n\nconst text = (t: string) =&gt; ({ content: [{ type: \"text\" as const, text: t }] });\nconst readOnly = { readOnlyHint: true, destructiveHint: false, openWorldHint: false };\n\nfunction createServer() {\n  const server = new McpServer({ name: \"project-checks\", version: \"0.1.0\" });\n\n  server.registerTool(\n    \"composer_validate\",\n    {\n      description: \"Run composer validate --strict in the project. Changes nothing.\",\n      inputSchema: z.object({}),\n      annotations: readOnly,\n    },\n    async () =&gt; text(await ddev([\"composer\", \"validate\", \"--strict\", \"--no-check-publish\"])),\n  );\n\n  server.registerTool(\n    \"phpstan\",\n    {\n      description: \"Run PHPStan on one of the allowed paths and return the raw report.\",\n      inputSchema: z.object({\n        path: z.enum([\"Source\/Extension\", \"packages\"]).describe(\"Directory to analyse\"),\n        level: z.number().int().min(0).max(10).default(5),\n      }),\n      annotations: readOnly,\n    },\n    async ({ path, level }) =&gt;\n      text(await ddev([\n        \"vendor\/bin\/phpstan\", \"analyse\", path,\n        `--level=${level}`, \"--no-progress\", \"--error-format=raw\",\n      ], 300_000)),\n  );\n\n  server.registerTool(\n    \"smoke_check\",\n    {\n      description: \"Request one page of the local site; report HTTP status, size and error markers.\",\n      inputSchema: z.object({\n        path: z.string().regex(\/^\/(?!\/)[A-Za-z0-9\/_.-]*$\/).default(\"\/\").describe(\"Path, e.g. \/blog\/\"),\n      }),\n      annotations: readOnly,\n    },\n    async ({ path }) =&gt; {\n      const url = new URL(path, BASE_URL);\n      if (url.origin !== BASE_URL.origin) {\n        return text(`Refused: ${url} is not on ${BASE_URL.origin}`);\n      }\n      const response = await fetch(url, { redirect: \"manual\" });\n      const body = await response.text();\n      const markers = [\"Oops, an error occurred\", \"Fatal error\", \"Uncaught\", \"There has been a critical error\"]\n        .filter((marker) =&gt; body.includes(marker));\n      return text(`${url} -&gt; ${response.status}, ${body.length} bytes, markers: ${markers.join(\", \") || \"none\"}`);\n    },\n  );\n\n  return server;\n}\n\nserveStdio(createServer, {\n  onerror: (error) =&gt; console.error(\"project-checks error:\", error),\n});\nconsole.error(\"project-checks running on stdio\");<\/code><\/pre>\n<p>A few details that matter:<\/p>\n<ul>\n<li><strong>No <code>console.log()<\/code>.<\/strong> A stdio server talks JSON-RPC over stdout. One stray log line there breaks the protocol. Log to stderr with <code>console.error()<\/code>.<\/li>\n<li><strong><code>serveStdio<\/code> takes a factory.<\/strong> The current SDK calls <code>createServer()<\/code> whenever it needs an instance, so the function must return a new <code>McpServer<\/code> each time. The older <code>server.connect(new StdioServerTransport())<\/code> wiring still compiles, but according to the quickstart it only serves clients that use the older handshake.<\/li>\n<li><strong>Annotations are hints.<\/strong> <code>readOnlyHint: true<\/code> tells the client what the tool does; it enforces nothing. What makes the tool read-only is its code: fixed binaries, fixed arguments, an enum for paths.<\/li>\n<li><strong>Validate inputs twice.<\/strong> An easy mistake is a <code>smoke_check<\/code> that accepts any path starting with a slash. A path like <code>\/\/example.com<\/code> is a protocol-relative URL, and <code>new URL()<\/code> happily resolves it to another host. The regex above rejects a second slash at the start, and the origin check catches anything I did not think of.<\/li>\n<\/ul>\n<p>PHPStan has to be installed in the project (<code>composer require --dev phpstan\/phpstan<\/code>), otherwise the tool just reports the missing binary. hoja.in itself has no PHPStan yet; the tool is generic.<\/p>\n<h2>Step 3: build and test without an agent<\/h2>\n<p>Test the server before any AI gets near it. The MCP Inspector has a CLI mode:<\/p>\n<pre><code class=\"language-bash\">npm run build\nnpx @modelcontextprotocol\/inspector --cli node build\/index.js --protocol-era modern --method tools\/list\nnpx @modelcontextprotocol\/inspector --cli node build\/index.js \n  --method tools\/call --tool-name smoke_check --tool-arg path=\/blog\/<\/code><\/pre>\n<p><code>tools\/list<\/code> returns the three tools with their JSON schemas: the enum for <code>path<\/code>, minimum and maximum for <code>level<\/code>, and the annotations. That is what the model sees, so read it once with the eyes of somebody who does not know your project.<\/p>\n<h2>Step 4: register it in .mcp.json<\/h2>\n<p>For Claude Code, a project-scoped server goes into <code>.mcp.json<\/code> at the project root, which you commit. Environment variables can be expanded with <code>${VAR}<\/code> or <code>${VAR:-default}<\/code>. The relative path and the <code>process.cwd()<\/code> fallback assume the server is started from the project root; if your setup starts it elsewhere, set <code>PROJECT_DIR<\/code> and use an absolute path:<\/p>\n<pre><code class=\"language-json\">{\n  \"mcpServers\": {\n    \"project-checks\": {\n      \"command\": \"node\",\n      \"args\": [\"tools\/project-checks\/build\/index.js\"],\n      \"env\": {\n        \"BASE_URL\": \"${PROJECT_BASE_URL:-https:\/\/myport.ddev.site}\"\n      }\n    }\n  }\n}<\/code><\/pre>\n<p>Claude Code asks for approval before it uses a project-scoped server in an interactive session, which is right: a cloned repository should not start processes on your machine silently. After approval the tools appear as <code>mcp__project-checks__composer_validate<\/code> and so on. Allow them without prompts in <code>.claude\/settings.json<\/code>:<\/p>\n<pre><code class=\"language-json\">{\n  \"permissions\": {\n    \"allow\": [\"mcp__project-checks__*\"]\n  }\n}<\/code><\/pre>\n<p>An allow rule with a wildcard is only accepted after the literal <code>mcp__&lt;server&gt;__<\/code> prefix, so you cannot accidentally allow every MCP tool of every server this way.<\/p>\n<h2>What this does not protect<\/h2>\n<p>Be honest about the boundary. The MCP server runs as your user, outside Claude Code&#8217;s Bash sandbox, so its own code must be trustworthy. And if the agent still has an unrestricted Bash tool next to it, the MCP server is a convenience, not a security measure. The value comes when you combine it with permissions: allow the three MCP tools, keep Bash on &#8220;ask&#8221; or deny the commands they replace. Then the routine checks run without interruptions, and every prompt you see is about something new.<\/p>\n<p>If this post helped you, I will be really happy. Cheers!<\/p>\n<h2>Further reading<\/h2>\n<ul>\n<li><a href=\"https:\/\/modelcontextprotocol.io\/docs\/develop\/build-server\">MCP: build a server (TypeScript quickstart)<\/a><\/li>\n<li><a href=\"https:\/\/code.claude.com\/docs\/en\/mcp\">Claude Code: connect to MCP servers<\/a><\/li>\n<li><a href=\"https:\/\/code.claude.com\/docs\/en\/permissions\">Claude Code: configure permissions<\/a><\/li>\n<li><a href=\"https:\/\/nodejs.org\/api\/child_process.html#child_processexecfilefile-args-options-callback\">Node.js: child_process.execFile<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Instead of giving an agent a shell, give it three safe tools. A small MCP server in TypeScript that runs composer validate, PHPStan and a frontend smoke check, registered in .mcp.json.<\/p>\n","protected":false},"author":1,"featured_media":107,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[18],"tags":[34,37,44,43],"class_list":["post-106","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ai-engineering","tag-ai-agents","tag-automation","tag-mcp","tag-security"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.7 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>A tiny read-only MCP server for project checks<\/title>\n<meta name=\"description\" content=\"Build a small MCP server with the official TypeScript SDK that exposes composer validate, PHPStan and a smoke check as read-only tools for Claude Code.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/hoja.in\/blog\/a-tiny-mcp-server-that-exposes-read-only-project-checks\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"A tiny read-only MCP server for project checks\" \/>\n<meta property=\"og:description\" content=\"Build a small MCP server with the official TypeScript SDK that exposes composer validate, PHPStan and a smoke check as read-only tools for Claude Code.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/hoja.in\/blog\/a-tiny-mcp-server-that-exposes-read-only-project-checks\/\" \/>\n<meta property=\"og:site_name\" content=\"Hoja Mustaffa Abdul Latheef \u2013 Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-10-31T15:00:00+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/hoja.in\/blog\/wp-content\/uploads\/2026\/10\/a-tiny-mcp-server-that-exposes-read-only-project-checks.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"630\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"Hoja Mustaffa Abdul Latheef\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:creator\" content=\"@hojaonline\" \/>\n<meta name=\"twitter:site\" content=\"@hojaonline\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Hoja Mustaffa Abdul Latheef\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"6 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/a-tiny-mcp-server-that-exposes-read-only-project-checks\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/a-tiny-mcp-server-that-exposes-read-only-project-checks\\\/\"},\"author\":{\"name\":\"Hoja Mustaffa Abdul Latheef\",\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/#\\\/schema\\\/person\\\/e6fbd54c2061674d8ea3e7861d858c7b\"},\"headline\":\"A tiny MCP server that exposes read-only project checks\",\"datePublished\":\"2026-10-31T15:00:00+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/a-tiny-mcp-server-that-exposes-read-only-project-checks\\\/\"},\"wordCount\":767,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/#\\\/schema\\\/person\\\/e6fbd54c2061674d8ea3e7861d858c7b\"},\"image\":{\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/a-tiny-mcp-server-that-exposes-read-only-project-checks\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/hoja.in\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/a-tiny-mcp-server-that-exposes-read-only-project-checks.jpg\",\"keywords\":[\"AI agents\",\"Automation\",\"MCP\",\"Security\"],\"articleSection\":[\"AI engineering\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/hoja.in\\\/blog\\\/a-tiny-mcp-server-that-exposes-read-only-project-checks\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/a-tiny-mcp-server-that-exposes-read-only-project-checks\\\/\",\"url\":\"https:\\\/\\\/hoja.in\\\/blog\\\/a-tiny-mcp-server-that-exposes-read-only-project-checks\\\/\",\"name\":\"A tiny read-only MCP server for project checks\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/a-tiny-mcp-server-that-exposes-read-only-project-checks\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/a-tiny-mcp-server-that-exposes-read-only-project-checks\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/hoja.in\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/a-tiny-mcp-server-that-exposes-read-only-project-checks.jpg\",\"datePublished\":\"2026-10-31T15:00:00+00:00\",\"description\":\"Build a small MCP server with the official TypeScript SDK that exposes composer validate, PHPStan and a smoke check as read-only tools for Claude Code.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/a-tiny-mcp-server-that-exposes-read-only-project-checks\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/hoja.in\\\/blog\\\/a-tiny-mcp-server-that-exposes-read-only-project-checks\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/a-tiny-mcp-server-that-exposes-read-only-project-checks\\\/#primaryimage\",\"url\":\"https:\\\/\\\/hoja.in\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/a-tiny-mcp-server-that-exposes-read-only-project-checks.jpg\",\"contentUrl\":\"https:\\\/\\\/hoja.in\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/a-tiny-mcp-server-that-exposes-read-only-project-checks.jpg\",\"width\":1200,\"height\":630,\"caption\":\"Three safe tools, no shell\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/a-tiny-mcp-server-that-exposes-read-only-project-checks\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/hoja.in\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"A tiny MCP server that exposes read-only project checks\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/hoja.in\\\/blog\\\/\",\"name\":\"Hoja Mustaffa Abdul Latheef \u2013 Blog\",\"description\":\"Notes from daily work with TYPO3, PHP and AI-assisted engineering\",\"publisher\":{\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/#\\\/schema\\\/person\\\/e6fbd54c2061674d8ea3e7861d858c7b\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/hoja.in\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/#\\\/schema\\\/person\\\/e6fbd54c2061674d8ea3e7861d858c7b\",\"name\":\"Hoja Mustaffa Abdul Latheef\",\"logo\":{\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/#\\\/schema\\\/person\\\/image\\\/\"},\"description\":\"Senior TYPO3 Developer at jweiland.net, TYPO3 CMS Certified Developer. I write about TYPO3 upgrades, extension development, PHP and AI-assisted engineering with Claude Code.\",\"sameAs\":[\"https:\\\/\\\/hoja.in\\\/\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"A tiny read-only MCP server for project checks","description":"Build a small MCP server with the official TypeScript SDK that exposes composer validate, PHPStan and a smoke check as read-only tools for Claude Code.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/hoja.in\/blog\/a-tiny-mcp-server-that-exposes-read-only-project-checks\/","og_locale":"en_US","og_type":"article","og_title":"A tiny read-only MCP server for project checks","og_description":"Build a small MCP server with the official TypeScript SDK that exposes composer validate, PHPStan and a smoke check as read-only tools for Claude Code.","og_url":"https:\/\/hoja.in\/blog\/a-tiny-mcp-server-that-exposes-read-only-project-checks\/","og_site_name":"Hoja Mustaffa Abdul Latheef \u2013 Blog","article_published_time":"2026-10-31T15:00:00+00:00","og_image":[{"width":1200,"height":630,"url":"https:\/\/hoja.in\/blog\/wp-content\/uploads\/2026\/10\/a-tiny-mcp-server-that-exposes-read-only-project-checks.jpg","type":"image\/jpeg"}],"author":"Hoja Mustaffa Abdul Latheef","twitter_card":"summary_large_image","twitter_creator":"@hojaonline","twitter_site":"@hojaonline","twitter_misc":{"Written by":"Hoja Mustaffa Abdul Latheef","Est. reading time":"6 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/hoja.in\/blog\/a-tiny-mcp-server-that-exposes-read-only-project-checks\/#article","isPartOf":{"@id":"https:\/\/hoja.in\/blog\/a-tiny-mcp-server-that-exposes-read-only-project-checks\/"},"author":{"name":"Hoja Mustaffa Abdul Latheef","@id":"https:\/\/hoja.in\/blog\/#\/schema\/person\/e6fbd54c2061674d8ea3e7861d858c7b"},"headline":"A tiny MCP server that exposes read-only project checks","datePublished":"2026-10-31T15:00:00+00:00","mainEntityOfPage":{"@id":"https:\/\/hoja.in\/blog\/a-tiny-mcp-server-that-exposes-read-only-project-checks\/"},"wordCount":767,"commentCount":0,"publisher":{"@id":"https:\/\/hoja.in\/blog\/#\/schema\/person\/e6fbd54c2061674d8ea3e7861d858c7b"},"image":{"@id":"https:\/\/hoja.in\/blog\/a-tiny-mcp-server-that-exposes-read-only-project-checks\/#primaryimage"},"thumbnailUrl":"https:\/\/hoja.in\/blog\/wp-content\/uploads\/2026\/10\/a-tiny-mcp-server-that-exposes-read-only-project-checks.jpg","keywords":["AI agents","Automation","MCP","Security"],"articleSection":["AI engineering"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/hoja.in\/blog\/a-tiny-mcp-server-that-exposes-read-only-project-checks\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/hoja.in\/blog\/a-tiny-mcp-server-that-exposes-read-only-project-checks\/","url":"https:\/\/hoja.in\/blog\/a-tiny-mcp-server-that-exposes-read-only-project-checks\/","name":"A tiny read-only MCP server for project checks","isPartOf":{"@id":"https:\/\/hoja.in\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/hoja.in\/blog\/a-tiny-mcp-server-that-exposes-read-only-project-checks\/#primaryimage"},"image":{"@id":"https:\/\/hoja.in\/blog\/a-tiny-mcp-server-that-exposes-read-only-project-checks\/#primaryimage"},"thumbnailUrl":"https:\/\/hoja.in\/blog\/wp-content\/uploads\/2026\/10\/a-tiny-mcp-server-that-exposes-read-only-project-checks.jpg","datePublished":"2026-10-31T15:00:00+00:00","description":"Build a small MCP server with the official TypeScript SDK that exposes composer validate, PHPStan and a smoke check as read-only tools for Claude Code.","breadcrumb":{"@id":"https:\/\/hoja.in\/blog\/a-tiny-mcp-server-that-exposes-read-only-project-checks\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/hoja.in\/blog\/a-tiny-mcp-server-that-exposes-read-only-project-checks\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/hoja.in\/blog\/a-tiny-mcp-server-that-exposes-read-only-project-checks\/#primaryimage","url":"https:\/\/hoja.in\/blog\/wp-content\/uploads\/2026\/10\/a-tiny-mcp-server-that-exposes-read-only-project-checks.jpg","contentUrl":"https:\/\/hoja.in\/blog\/wp-content\/uploads\/2026\/10\/a-tiny-mcp-server-that-exposes-read-only-project-checks.jpg","width":1200,"height":630,"caption":"Three safe tools, no shell"},{"@type":"BreadcrumbList","@id":"https:\/\/hoja.in\/blog\/a-tiny-mcp-server-that-exposes-read-only-project-checks\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/hoja.in\/blog\/"},{"@type":"ListItem","position":2,"name":"A tiny MCP server that exposes read-only project checks"}]},{"@type":"WebSite","@id":"https:\/\/hoja.in\/blog\/#website","url":"https:\/\/hoja.in\/blog\/","name":"Hoja Mustaffa Abdul Latheef \u2013 Blog","description":"Notes from daily work with TYPO3, PHP and AI-assisted engineering","publisher":{"@id":"https:\/\/hoja.in\/blog\/#\/schema\/person\/e6fbd54c2061674d8ea3e7861d858c7b"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/hoja.in\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/hoja.in\/blog\/#\/schema\/person\/e6fbd54c2061674d8ea3e7861d858c7b","name":"Hoja Mustaffa Abdul Latheef","logo":{"@id":"https:\/\/hoja.in\/blog\/#\/schema\/person\/image\/"},"description":"Senior TYPO3 Developer at jweiland.net, TYPO3 CMS Certified Developer. I write about TYPO3 upgrades, extension development, PHP and AI-assisted engineering with Claude Code.","sameAs":["https:\/\/hoja.in\/"]}]}},"_links":{"self":[{"href":"https:\/\/hoja.in\/blog\/wp-json\/wp\/v2\/posts\/106","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/hoja.in\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/hoja.in\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/hoja.in\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/hoja.in\/blog\/wp-json\/wp\/v2\/comments?post=106"}],"version-history":[{"count":1,"href":"https:\/\/hoja.in\/blog\/wp-json\/wp\/v2\/posts\/106\/revisions"}],"predecessor-version":[{"id":176,"href":"https:\/\/hoja.in\/blog\/wp-json\/wp\/v2\/posts\/106\/revisions\/176"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/hoja.in\/blog\/wp-json\/wp\/v2\/media\/107"}],"wp:attachment":[{"href":"https:\/\/hoja.in\/blog\/wp-json\/wp\/v2\/media?parent=106"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/hoja.in\/blog\/wp-json\/wp\/v2\/categories?post=106"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/hoja.in\/blog\/wp-json\/wp\/v2\/tags?post=106"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}