For LLMs and Developers — This guide explains how to build a new MCP server tool by creating an “aspect” — a single .cpp file that plugs into the MCPServer framework.

What is an Aspect?

The MCPServer is a C++ implementation of the Model Context Protocol. Each aspect is a single .cpp file that defines the server’s personality: its name, tools, prompts, and handlers. Each aspect compiles into its own executable (e.g., MCPServer_helloWorld.exe, MCPServer_portScan.exe).

The framework (MCPServer.cpp) handles all JSON-RPC protocol mechanics — parsing requests, dispatching methods, validating output, stdin/stdout I/O. Your aspect only implements the what, never the how of the protocol.

Reference Templates

When building a new aspect, start by copying one of our baseline templates depending on the architecture you need:

  • Baseline Tool (MCPServer_helloWorld.cpp): A minimal, working aspect for standard tool execution. Start here if your tool just needs to execute local C code and return text sequentially.

  • Asynchronous Background Processing (MCPServer_setReminder.cpp): Start here if your tool requires non-blocking execution (like long-running jobs or delayed events). It demonstrates how to set MCPServer_Asynchronous = true, spawn background threads, and safely send notifications back to the client long after the initial request has finished.

  • Bi-directional LLM-code integration aspect (MCPServer_wordArt.cpp): Start here if you are building tools that need to natively query the client’s AI. It demonstrates how to use the classic MCP Sampling flow (sampling/createMessage) and the modern stateless MRTR flow (input_required) to delegate tasks (like cognitive reasoning or complex formatting) back to an LLM.

Required Sections (in order)

1. Includes and Identity

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <LevelTrace.h>
#include <MEMprintf.h>

#include <MCPServerCore.h>

const char *MCPServer_Name = "MCPServer_myAspect";     // Executable identity.
const char *MCPServer_Version = "0.1.0";
const char *MCPServer_Capabilities = "\"tools\":{}";    // MCP capabilities.
bool MCPServer_Asynchronous = false;                    // true for async tools.
  • MCPServer_Name — used in trace filenames and serverInfo sent to the client.
  • MCPServer_Capabilities — typically "\"tools\":{}". Add "prompts", "resources", etc. as needed.
  • MCPServer_Asynchronous — set true if any tool needs deferred responses (worker threads).

2. Test Cases

static const char* MCPInputTestRequest_MyCall =
        "{"
         " \"jsonrpc\": \"2.0\","
         " \"id\": 3,"
         " \"method\": \"tools/call\","
         " \"params\": {"
         "   \"name\": \"myTool\","
         "   \"arguments\": { \"key\": \"value\" }"
         " }"
         "}";

const char* MCPInputTestRequests[] =
        {
        MCPInputTestRequest_MyCall,
        NULL                            // NULL-terminated array.
        };

Run tests with: MCPServer_myAspect.exe Test Note: A successful test will output the raw JSON-RPC response directly to the console.

3. Init / Shutdown

bool MCPServer_OnStartup()
        {
        // Called once at process startup, before the message loop.
        // Register advanced hooks and always-on setup here.
        return true;            // Return false to abort startup.
        }

bool MCPServer_OnInitialize(MCPInputRequest &MCPRequest)
        {
        // Called once when the MCP client sends "initialize".
        return true;
        }

bool MCPServer_OnShutdown()
        {
        // Called when stdin closes (client disconnect).
        return true;
        }

4. Tool Directory

Register tools via the MCPToolInfo array. The framework auto-generates the tools/list response from this.

const unsigned MCPToolInfoCount = 1;

MCPToolInfo_t MCPToolInfo[] =
        {
                {
                // Name
                "myTool",
                // Description (one line, no newlines — token-efficient)
                "Does something useful. Returns result as text.",
                1,                      // nProperties
                // InputSchema {Properties}
                        {
                        "\"key\":{\"type\":\"string\","
                                 "\"description\":\"The input key\"}"
                        },
                // [Required]
                "\"key\""
                }
        };

5. Tool Handlers — The Two-Layer Pattern

Every tool follows this pattern:

MCPtool_xxx()           — Parse arguments, validate, call FormMCPResponse_xxx()
FormMCPResponse_xxx()   — Build result text, call FormMCPResponse_Text()

Memory Management Note: You do not need to manage the memory of the char* buffer returned by these handlers. Function handlers returning an MCPOutput JSON buffer must assume it will be automatically freed by the caller (MCPServer.cpp) after being sent.

Response builder — builds the text and wraps it:

char* FormMCPResponse_myTool(JSON_Value &idRequest, const char *Key)
        {
        MemoryPrintf Summary;
        Summary.printf("Result for key: %s", Key);
        return FormMCPResponse_Text(&idRequest, Summary.GetBuffer());
        }

Tool handler — parses and validates the request:

char* MCPtool_myTool(JSON_Value &idRequest, JSON_Object &Arguments)
        {
        const char *ProcName = "MCPtool_myTool";
        const char *Key = Arguments.Find_member_string("key");
        if (!Key)
                {
                TERROR(("%s: Missing Argument: 'key'",ProcName));
                return FormMCPResponse_ERROR(&idRequest,
                                             MCPErrorCode_InvalidParms,
                                             "Invalid Params",
                                             "Missing parameter: key");
                }
        return FormMCPResponse_myTool(idRequest, Key);
        }

6. Tool Dispatch

Route tool names to handlers:

char* Handle_tools_call(JSON_Value &idRequest,
                        const char *ToolName,
                        JSON_Object &Arguments)
        {
        char *MCPOutput = NULL;
        if (strcmp(ToolName,"myTool") == 0)
                {
                MCPOutput = MCPtool_myTool(idRequest, Arguments);
                }
        return MCPOutput;       // NULL = "unknown tool" (framework handles).
        }

7. Dormant Handlers

These are required by the linker. Copy them verbatim from MCPServer_helloWorld.cpp and activate as needed:

void Handle_notification(MCPInputRequest &MCPRequest) { return; }
char* Handle_sampling_response(MCPInputRequest &MCPRequest) { return NULL; }
char* Handle_tools_call_resume(MCPInputRequest &MCPRequest) { return NULL; }
char* Handle_notify_action(void *UserPtr) { return NULL; }
void Discard_notify_action(void *UserPtr) { return; }
char* Handle_completion_complete(JSON_Value &idRequest, const char *RefType, const char *RefName, const char *ArgName, const char *ArgValue) { return NULL; }
const unsigned MCPPromptInfoCount = 0;
MCPPromptInfo_t MCPPromptInfo[] = { {0} };
char* Handle_prompts_get(JSON_Value &idRequest, const char *PromptName, JSON_Object &Arguments) { return NULL; }
char* Handle_method(MCPInputRequest &MCPRequest) { return NULL; }

Startup: Hooks and Always-On Setup

MCPServer_OnStartup() runs once at process startup, before the message loop and before any request is dispatched. It is where an aspect performs setup that must always be in place regardless of how a client connects — a stateless 2026-07-28 client discovers the server via server/discover and never calls MCPServer_OnInitialize(), so anything needed on every request belongs here:

  • Advanced hooks — register Handle_tools_call_hook (and the prompt and completion hooks) here.
  • Attribution — set MCPServer_AspectCredit to the aspect’s credit string; the about_tsar-mcp tool reports it.
  • Environment — read environment variables and resolve paths.

Reserve MCPServer_OnInitialize() for logic that genuinely needs the client’s initialize parameters (it only fires for stateful clients).

MCP Protocol Considerations

Protocol Version Selection (ClampMCPProtocolVersion_20251125)

The build-time macro ClampMCPProtocolVersion_20251125 (defined in MCPServerCore.h, overridable per build with -DClampMCPProtocolVersion_20251125=…) selects the highest protocol version the server advertises and speaks by expanding MCPProtocolVersion_ServerMax:

Value Server ceiling Effect
1 — shipping default 2025-11-25 (stateful) The stateless 2026-07-28 lane is compiled out: server/discover is removed and MCPProtocolVersion_ServerMax caps at 2025-11-25.
0 2026-07-28 (stateless) Enables the 2026-07-28 protocol — server/discover, per-request _meta versioning, and MRTR input-required round trips.

The default ships as 1. The 2026-07-28 stateless/MRTR support is currently a release candidate; clamping to 2025-11-25 keeps production builds on the proven stateful lane until 2026-07-28 is promoted to stable. Build with -DClampMCPProtocolVersion_20251125=0 to opt a binary into the release candidate.

The clamp is a ceiling, not a floor — a clamped server still negotiates down to older clients. The per-request MCPProtocolVersion() accessor clamps each client’s requested version to MCPProtocolVersion_ServerMax, so an aspect never has to test the macro itself; it simply reports and speaks whatever version the framework resolves for the request (as the about_tsar-mcp tool’s Client/Server/Speak lines illustrate).

Migrating to the 2026-07-28 Protocol

Two steps bring an older aspect up to the 2026-07-28 framework:

1. Add the two now-required stubs. MCPServer_OnStartup() and Handle_tools_call_resume() became link-required; an aspect missing either fails to link — a loud, explicit missing-symbol error, never a silent runtime break:

// Process startup hook.
bool MCPServer_OnStartup() { return true; }

// Input-required resume (dormant).
char* Handle_tools_call_resume(MCPInputRequest &MCPRequest) { return NULL; }

Paste both verbatim; wire them up only if the aspect needs startup work or input-required round trips.

2. Move initialization from MCPServer_OnInitialize() to MCPServer_OnStartup(), leaving only logic that needs the client’s initialize parameters in MCPServer_OnInitialize() (see Startup: Hooks and Always-On Setup above).

Build System

To add a new aspect, add it to both Makefiles in the mcp/ directory:

GNU Make (Makefile) — Linux:

# Named target:
myAspect: _makedirs $(OUTDIR)/MCPServer_myAspect

# Link rule:
$(OUTDIR)/MCPServer_myAspect: $(SHARED_OBJS) $(O)/MCPServer_myAspect.o
	$(CXX) $(LDFLAGS) $^ -o $@ $(LIBS)

# Compile rule:
$(O)/%.o: $(MCPSRV_DIR)/myAspect/%.cpp
	$(CXX) $(CXXFLAGS) $(INC_CORE) -c $< -o $@

Build: make myAspect (or make myAspect CFG=Debug)

NMAKE (Makefile.nmake) — Windows:

# Named target:
myAspect: _makedirs $(OUTDIR)\MCPServer_myAspect.exe

# Link rule:
$(OUTDIR)\MCPServer_myAspect.exe: $(SHARED_OBJS) $(O)\MCPServer_myAspect.obj
    $(LINK) $(LDFLAGS) /OUT:$@ $** $(LIBS)

# Compile rule:
{$(MCPSRV_DIR)\myAspect\}.cpp{$(O)\}.obj:
    $(CC) $(CFLAGS) $(INC_CORE) /Fo$@ $<

Build: nmake myAspect (or nmake myAspect CFG=Debug)

AIX make (Makefile.aix) — AIX / IBM i, IBM XL C/C++:

Native AIX make has no pattern rules and leaves $</$^ empty, so name sources and objects in full.

# Named target:
myAspect:
	$(MAKE) -f Makefile.aix _makedirs $(OUTDIR)/MCPServer_myAspect CFG=$(CFG) CXXFLAGS_CFG="$(CXXFLAGS_CFG)"

# Link rule:
$(OUTDIR)/MCPServer_myAspect: $(SHARED_OBJS) $(O)/MCPServer_myAspect.o
	$(CXX) $(LDFLAGS) $(SHARED_OBJS) $(O)/MCPServer_myAspect.o -o $@ $(LIBS)

# Compile rule:
$(O)/MCPServer_myAspect.o: $(MCPSRV_DIR)/myAspect/MCPServer_myAspect.cpp
	$(CXX) $(CXXFLAGS) $(INC_CORE) -c $(MCPSRV_DIR)/myAspect/MCPServer_myAspect.cpp -o $@

Build: make -f Makefile.aix myAspect (or make -f Makefile.aix myAspect CFG=Debug)

Also add $(OUTDIR)/MCPServer_myAspect to ASPECTS. Makefile.aix must have LF line endings.

LLM Registration

VSCode mcp.json:

{
  "servers": {
    "myAspect": {
      "type": "stdio",
      "command": "path/to/MCPServer_myAspect.exe"
    }
  }
}

See also: MCPServer Wiring & Transport Guide

Advanced Native Features

The core framework (MCPServer.cpp) inherently supports:

  • Async Tools: Set MCPServer_Asynchronous = true and return Return_MCPOutput_Pending(). Use PostMCPOutput() or PostNotifyAction() from your worker thread.
  • Sampling: Send sampling/createMessage requests to the LLM and catch the result in Handle_sampling_response(). Use FormMCPRequest_sampling() to build a sampling request.
  • Prompts & Agentic Polyfill: Populate MCPPromptInfo[] and implement Handle_prompts_get(). Use FormMCPResponse_prompt() to respond to a prompt request. By default, the framework automatically polyfills these prompts as agentic services (listPrompts and getPrompt tools), allowing LLMs to seamlessly invoke them even if the client application lacks native prompt support. This behavior can be disabled via the -DIncludePromptTools=0 compiler flag.
  • Input-Required Round Trips (MCP 2026-07-28): Return an input_required result built with FormMCPResult_inputRequired() (or the requestState overload of FormMCPRequest_sampling()). The client resumes with a tools/call carrying requestState, which the framework routes to Handle_tools_call_resume(); returning NULL from the resume handler yields a standard -32602 error.
  • Custom main() (NO_MAIN_MCP): Define NO_MAIN_MCP to supply your own main() and drive the lifecycle explicitly via main_mcp_init(), main_mcp(), and main_mcp_deinit() — the same order the default MCPServer.cpp uses. Useful when the aspect must own process startup.

Checklist for a New Aspect

  • Copy MCPServer_helloWorld.cpp → MCPServer_myAspect.cpp
  • Set Identity (MCPServer_Name, MCPServer_Version)
  • Define MCPToolInfo[]
  • Implement FormMCPResponse_xxx() and MCPtool_xxx()
  • Wire dispatch in Handle_tools_call()
  • Add test cases and verify via MCPServer_myAspect.exe Test
  • Add Makefile target & LLM Registration