Skip to the content.

How to Create an MCPServer Aspect

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:

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.

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:

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
1shipping 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)

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:

Checklist for a New Aspect