ZyVOP Logo
Content That Connects
SeriesAI NewsWhy ZyVOPJoin Discord
LoginGet Started
ZyVOP Logo
Content That Connects

The Developer Publishing Hub. Write once, cross-post to Dev.to, Medium, Hashnode, WordPress & Bluesky with automated canonical source tags and zero paywalls.

Content

  • Categories
  • Tags
  • Badges
  • Leaderboard
  • Write Article
  • Newsletter

Company

  • About Us
  • Why ZyVOP
  • Developer API & CLI
  • Write for Us
  • Contact

Connect

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • DMCA Policy
  • Code of Conduct

ยฉ 2026 ZyVOP. Developer Publishing Hub.

Zero paywalls ยท Full content ownership
All systems operational
HomeImplementing A Simple MCP Hook: From Zero to Stdio-Connected Tools

Implementing A Simple MCP Hook: From Zero to Stdio-Connected Tools

Alain Airom (Ayrom)
Alain Airom (Ayrom)
Build Engineer
August 31, 2026
8 min read
Implementing A Simple MCP Hook: From Zero to Stdio-Connected Tools
#MCP#bob
๐Ÿ‘1

An introduction to MCP Hooks!

Introduction

The Model Context Protocol (MCP) has quickly become a standard for bridging Large Language Models with external tools, APIs, and data sources. While the protocol itself handles structured JSON-RPC messaging under the hood, seeing a end-to-end integration come together using a local stdio process โ€” a setup often referred to as an MCP Hook or integration โ€” is remarkably straight-forward.

In this post, weโ€™ll walk through building a custom MCP server in TypeScript, registering tools with schema validation, and wiring up a Node.js client to spawn, negotiate, and execute tool calls end-to-end.


TL;DR-What is an MCP Hook? An Architectural Definition

At its core, a Model Context Protocol (MCP) Hook is an integration pattern that bridges a Large Language Model (LLM) host with external execution environments. While LLMs excel at reasoning, they operate in isolation from live data and system state. MCP solves this by establishing a standardized protocol for tool discovery, context injection, and function calling.

The term MCP Hook specifically describes the runtime linkage and communication channel established between the client application (the MCP Host) and a background tool server (the MCP Server):

  • Protocol Framing: Rather than making remote network calls over HTTP/REST, local MCP hooks typically leverage low-latency process communication (such as standard I/O streams: stdin and stdout).

  • Deterministic Delegation: The client process spawns the tool server as a child process, negotiates capabilities via a structured JSON-RPC 2.0 handshake, and dynamically dispatches function calls based on the modelโ€™s intent.

  • Process Isolation: By decoupling tool execution into a separate runtime process, the hook ensures that file I/O, API calls, or hardware manipulations happen safely outside the primary host application thread.

In essence, an MCP Hook transforms an LLM from a passive text-generation engine into an active agent capable of deterministically inspecting time, querying databases, executing shell operations, or interacting with host resources.


Implementation-System Architecture & Lifecycle

**An MCP Hook **connection over standard input/output (stdio) operates through a parent-child process relationship:

  • Host/Client Initialization: The client process spawns the MCP server executable as a child process.

  • Protocol Handshake: Standard I/O streams (stdin/stdout) frame JSON-RPC 2.0 requests to negotiate capacities and versions.

  • Tool Discovery & Call: The client queries available capabilities (listTools) and executes handlers (callTool).

  • Graceful Teardown: Closing the transport terminates the child process safely.


Server Implementation: Schemas & Tool Handlers

The server uses @modelcontextprotocol/sdk alongside zod to enforce strict parameter schemas before any tool handler is invoked.

Tip: When using a stdio transport, never output logging or debugging messages to console.log on the server. Doing so corrupts the stdout JSON-RPC message framing. Always redirect operational logs to console.error.

  • Server Entry Point (index.ts);

#!/usr/bin/env node
/**
 * mcp-hook-server โ€” Demonstration MCP Server
 *
 * Exposes three tools:
 *   - echo        : returns the input message back
 *   - current-time: returns the current server date/time in the requested timezone
 *   - random-joke : returns a random programming joke
 *
 * Transport: stdio (spawned by an MCP host such as Bob)
 */import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";import { z } from "zod";// โ”€โ”€โ”€ Joke dataset โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€const JOKES = [
    "Why do programmers prefer dark mode? Because light attracts bugs.",
    "A SQL query walks into a bar, walks up to two tables and asksโ€ฆ 'Can I join you?'",
    "How many programmers does it take to change a light bulb? None โ€” that's a hardware problem.",
    "Why do Java developers wear glasses? Because they don't C#.",
    "There are 10 types of people in the world: those who understand binary and those who don't.",
    "A byte walks into a bar looking pale. The barman asks: 'What's wrong?' It replies: 'I had a bit removed.'",
    "Why was the developer unhappy at their job? They wanted arrays.",
    "I would tell you a UDP joke, but you might not get it.",];// โ”€โ”€โ”€ Server setup โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€const server = new McpServer({
    name: "mcp-hook-server",
    version: "0.1.0",});// โ”€โ”€โ”€ Tool: echo โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€server.registerTool("echo", {
    description: "Echoes back the provided message. Useful for verifying that the MCP hook is working end-to-end.",
    inputSchema: z.object({
        message: z.string().describe("The message to echo back"),
    }),}, async ({ message }) => {
    return {
        content: [
            {
                type: "text",
                text: `[MCP echo] ${message}`,
            },
        ],
    };});// โ”€โ”€โ”€ Tool: current-time โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€server.registerTool("current-time", {
    description: "Returns the current date and time on the server.",
    inputSchema: z.object({
        timezone: z
            .string()
            .optional()
            .describe("IANA timezone name (e.g. 'Europe/Paris'). Defaults to UTC."),
        format: z
            .enum(["iso", "human"])
            .optional()
            .describe("Output format: 'iso' (default) or 'human'-readable."),
    }),}, async ({ timezone, format }) => {
    const tz = timezone ?? "UTC";
    const fmt = format ?? "iso";
    let result;
    try {
        const now = new Date();
        if (fmt === "human") {
            result = now.toLocaleString("en-US", {
                timeZone: tz,
                dateStyle: "full",
                timeStyle: "long",
            });
        }
        else {
            result = now
                .toLocaleString("sv-SE", { timeZone: tz })
                .replace(" ", "T");
        }
    }
    catch {
        return {
            content: [
                {
                    type: "text",
                    text: `Unknown timezone: '${tz}'. Please use a valid IANA timezone name.`,
                },
            ],
            isError: true,
        };
    }
    return {
        content: [{ type: "text", text: result }],
    };});// โ”€โ”€โ”€ Tool: random-joke โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€server.registerTool("random-joke", {
    description: "Returns a random programming or developer joke.",
    inputSchema: z.object({}),}, async () => {
    const joke = JOKES[Math.floor(Math.random() * JOKES.length)];
    return {
        content: [{ type: "text", text: joke }],
    };});// โ”€โ”€โ”€ Start โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€async function main() {
    const transport = new StdioServerTransport();
    await server.connect(transport);
    console.error("mcp-hook-server running on stdio");}main().catch((error) => {
    console.error("Fatal error in mcp-hook-server:", error);
    process.exit(1);});

Enter fullscreen mode Exit fullscreen mode

Client Driver & Verification

To test the hook, the client process uses StdioClientTransport to target the compiled server binary (build/index.js), list the registered tools, and execute them.

- Client Script (index.js);

/**
 * mcp-hook-client โ€” Demonstration MCP Client
 *
 * Spawns mcp-hook-server via stdio transport, discovers all available tools,
 * then calls each tool once to show an end-to-end MCP hook in action.
 *
 * Run: node src/index.js
 */

import { Client } from "@modelcontextprotocol/sdk/client/index.js";import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";import path from "node:path";import { fileURLToPath } from "node:url";

// โ”€โ”€โ”€ Resolve absolute path to the built server โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

const __dirname = path.dirname(fileURLToPath(import.meta.url));const SERVER_PATH = path.resolve(__dirname,"../../mcp-hook-server/build/index.js");

// โ”€โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

function banner(title) {const line = "โ”€".repeat(60);console.log(`\n${line}`);console.log(`  ${title}`);console.log(`${line}`);}

function printResult(toolName, result) {const text = result?.content
    ?.filter((c) => c.type === "text")
    .map((c) => c.text)
    .join("\n");console.log(`[${toolName}] โ†’`, text ?? JSON.stringify(result));}

// โ”€โ”€โ”€ Main demo โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

async function main() {banner("MCP Hook Demo โ€” Client connecting to mcp-hook-server");

  // 1. Create clientconst client = new Client({
    name: "mcp-hook-client",
    version: "0.1.0",});

  // 2. Create stdio transport โ€” spawns the server as a child processconst transport = new StdioClientTransport({
    command: "node",
    args: [SERVER_PATH],});

  // 3. Connectconsole.log("\n[client] Connecting to serverโ€ฆ");await client.connect(transport);console.log("[client] Connected โœ“");

  // 4. List available toolsconst { tools } = await client.listTools();console.log(
    `\n[client] Server exposes ${tools.length} tool(s):`,
    tools.map((t) => t.name).join(", "));

  // โ”€โ”€ Demo: echo โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€banner("Tool demo: echo");const echoResult = await client.callTool({
    name: "echo",
    arguments: { message: "Hello from the MCP client!" },});printResult("echo", echoResult);

  // โ”€โ”€ Demo: current-time (ISO, UTC) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€banner("Tool demo: current-time (ISO / UTC)");const timeIso = await client.callTool({
    name: "current-time",
    arguments: {},});printResult("current-time", timeIso);

  // โ”€โ”€ Demo: current-time (human, Paris) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€banner("Tool demo: current-time (human / Europe/Paris)");const timeParis = await client.callTool({
    name: "current-time",
    arguments: { timezone: "Europe/Paris", format: "human" },});printResult("current-time", timeParis);

  // โ”€โ”€ Demo: random-joke โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€banner("Tool demo: random-joke");const joke = await client.callTool({ name: "random-joke", arguments: {} });printResult("random-joke", joke);

  // 5. Disconnectawait client.close();console.log("\n[client] Disconnected. Demo complete โœ“\n");}

main().catch((err) => {console.error("Demo client error:", err);process.exit(1);});

Enter fullscreen mode Exit fullscreen mode

Overall Test Script

To test the implementation, we can run a small test script.

- Test Script (test.js);

/**
 * mcp-hook-client โ€” Unit Tests
 *
 * Spawns the real mcp-hook-server and exercises every tool.
 * Exit code 0 = all passed. Exit code 1 = one or more failures.
 *
 * Run: node src/test.js
 */

import { Client } from "@modelcontextprotocol/sdk/client/index.js";import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";import path from "node:path";import { fileURLToPath } from "node:url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));const SERVER_PATH = path.resolve(__dirname,"../../mcp-hook-server/build/index.js");

// โ”€โ”€โ”€ Tiny test harness โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

let passed = 0;let failed = 0;

function assert(condition, label) {if (condition) {
    console.log(`  โœ“ ${label}`);
    passed++;} else {
    console.error(`  โœ— ${label}`);
    failed++;}}

function getText(result) {return result?.content
    ?.filter((c) => c.type === "text")
    .map((c) => c.text)
    .join("\n") ?? "";}

// โ”€โ”€โ”€ Tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

async function runTests(client) {// โ”€โ”€ listTools โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€console.log("\n[suite] listTools");const { tools } = await client.listTools();const names = tools.map((t) => t.name);assert(names.includes("echo"), "exposes 'echo' tool");assert(names.includes("current-time"), "exposes 'current-time' tool");assert(names.includes("random-joke"), "exposes 'random-joke' tool");assert(tools.length === 3, "exposes exactly 3 tools");

  // โ”€โ”€ echo โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€console.log("\n[suite] echo");const echo1 = await client.callTool({
    name: "echo",
    arguments: { message: "test-payload" },});assert(
    getText(echo1) === "[MCP echo] test-payload",
    "echoes message with prefix");assert(!echo1.isError, "echo returns no error flag");

  const echoEmpty = await client.callTool({
    name: "echo",
    arguments: { message: "" },});assert(getText(echoEmpty) === "[MCP echo] ", "echoes empty string correctly");

  // โ”€โ”€ current-time โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€console.log("\n[suite] current-time");const timeUtc = await client.callTool({
    name: "current-time",
    arguments: {},});const utcText = getText(timeUtc);assert(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(utcText), "UTC ISO format matches yyyy-mm-ddThh:mm");assert(!timeUtc.isError, "current-time UTC returns no error flag");

  const timeHuman = await client.callTool({
    name: "current-time",
    arguments: { timezone: "America/New_York", format: "human" },});assert(!timeHuman.isError, "human format (America/New_York) returns no error");assert(getText(timeHuman).length > 0, "human format returns non-empty string");

  const timeBad = await client.callTool({
    name: "current-time",
    arguments: { timezone: "Not/AReal_Zone" },});assert(timeBad.isError === true, "invalid timezone sets isError=true");

  // โ”€โ”€ random-joke โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€console.log("\n[suite] random-joke");const joke1 = await client.callTool({ name: "random-joke", arguments: {} });assert(!joke1.isError, "random-joke returns no error flag");assert(getText(joke1).length > 0, "random-joke returns non-empty text");

  // Ensure it can return at least two different jokes across 20 calls (non-deterministic โ€” might rarely fail)const jokeSamples = new Set();for (let i = 0; i < 20; i++) {
    const r = await client.callTool({ name: "random-joke", arguments: {} });
    jokeSamples.add(getText(r));}assert(jokeSamples.size > 1, "random-joke returns more than one unique joke across 20 calls");}

// โ”€โ”€โ”€ Bootstrap โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

async function main() {console.log("=== mcp-hook unit tests ===");

  const client = new Client({ name: "mcp-hook-test-client", version: "0.1.0" });const transport = new StdioClientTransport({
    command: "node",
    args: [SERVER_PATH],});await client.connect(transport);

  try {
    await runTests(client);} finally {
    await client.close();}

  console.log(`\n=== Results: ${passed} passed, ${failed} failed ===\n`);if (failed > 0) process.exit(1);}

main().catch((err) => {console.error("Test runner error:", err);process.exit(1);});

Enter fullscreen mode Exit fullscreen mode

  • Which provides a log file validating the whole logic of the implementation.

โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  MCP Hook Demo โ€” Client connecting to mcp-hook-server
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

[client] Connecting to serverโ€ฆ
mcp-hook-server running on stdio
[client] Connected โœ“

[client] Server exposes 3 tool(s): echo, current-time, random-joke

โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  Tool demo: current-time (human / Europe/Paris)
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
[current-time] โ†’ Monday, August 10, 2026 at 9:49:46 AM GMT+2

[client] Disconnected. Demo complete โœ“

Enter fullscreen mode Exit fullscreen mode


Integration into MCP Hosts (e.g., Bob)

Once compiled, registering your new custom server inside an agent or workspace host like Bob requires registering the process entry in .bob/mcp.json:

{"mcpServers": {
    "mcp-hook-server": {
      "command": "node",
      "args": ["/absolute/path/to/mcp-hook-server/build/index.js"]
    }}}

Enter fullscreen mode Exit fullscreen mode

This makes echo, current-time, and random-joke directly available to the AI assistant as native tool primitives during chat sessions.


Conclusion: Implementing Your First MCP Hook

Implementing your first MCP Hook demonstrates how remarkably clean decoupled LLM integrations can be. By relying on lightweight stdio transports, process isolation, and standard schemas via zod, you keep your tool logic testable, language-agnostic, and completely independent of any specific client UI or host framework.

Building local MCP hooks gives you full control over tool security, deterministic execution, and process lifecycle management โ€” laying a solid foundation for scaling up to complex agentic workflows.

Thanks for reading ๐Ÿช

Links

  • Official Model Context Protocol Specification & Docs: modelcontextprotocol.io

  • Official MCP TypeScript SDK: github.com/modelcontextprotocol/typescript-sdk

  • Official Reference Servers Repository: github.com/modelcontextprotocol/servers

  • civicteam/mcp-hooks: github.com/civicteam/mcp-hooks

  • Zod Official Website & Documentation: zod.dev

  • Zod GitHub Repository: github.com/colinhacks/zod

  • IBM Bob: https://bob.ibm.com/

Comments (0)

Login to post a comment.

Alain Airom (Ayrom)
Alain Airom (Ayrom)

Build Engineer

IT guy, IBMer... sharing my hands-on experiences and technical subjects of my interest (IBM or not). A bit "touche ร  tout"!

Subscribe to Alain Airom (Ayrom)'s Newsletter

More from Alain Airom (Ayrom)

View profile

Automating AI Context: How I Built a Custom Extension for IBM Bob IDE to Inject Project Rules

Building my own โ€œvsixโ€ extension to automate my own projects Introduction Setting up consistent workspace instructions across projects can quickly become a tedi...

10 minAug 31

Deep Dive: Testing Radar UI for Kubernetes using MCP, a Go GUI, and an Autonomous Agent

Hand-on test of radarhq.io K8S UI and dashboard on a macOS with Minikube and Podman Introduction Kubernetes dashboards are often either overloaded with unnecess...

5 minAug 31

Embedding Docling-NLP in Ad-Hoc UI Applications: A Lightweight Blueprint

Implementing a โ€œGraph Language Modelโ€ with Docling-NLP Image from Docling-AI Building a local, privacy-focused Document AI pipeline often feels like balancing h...

3 minAug 31

Loop Engineering 101

From Curiosity to Creation: Building a Loop Engineering Application with IBMโ€™s Bob Introduction For some time now, โ€œLoop Engineeringโ€ has been generating signif...

9 minAug 31

Building an OpenTelemetry Instrumentation Wizard

Accelerating observability adoption by automating OpenTelemetry instrumentation across heterogeneous codebases Introduction Years ago, I was tasked with buildin...

14 minAug 31