Become a member!

MCP for Delphi — Server, Client & Agent for DMVCFramework

MCP Server for DMVCFramework

Let your Delphi applications talk to AI assistants

MCP Protocol 2025-03-26 | GitHub Repository | Apache 2.0 License

GitHub starsLicenseDelphi

TL;DR: MCP Server for DMVCFramework is a full-stack MCP toolkit for Delphi. It is not just a server library — it gives you all three corners of the MCP world: a server (expose your Delphi business logic to Claude, Gemini, ChatGPT via [MCPTool] / [MCPResource] / [MCPPrompt] attributes), a client (TMCPClient / TMCPStdioClient to consume any MCP server from Delphi), and an agent (TMCPOpenAIAgent, a complete LLM agent loop). It even includes a REST→MCP bridge that turns an existing DMVCFramework API into MCP tools automatically. The library handles JSON schema generation, session management, JSON-RPC dispatch, and dual transport (HTTP + stdio). Copy a Quick Start project, customize the provider units, build, and connect.


What Is MCP and Why Should You Care?

The Model Context Protocol (MCP) is an open standard created by Anthropic that defines how AI assistants communicate with external software. Think of it as a universal plug that connects your application to any AI client — Claude, Gemini, ChatGPT, Cursor, Continue, and dozens more.

With MCP, your Delphi application can:

  • Expose tools — functions the AI can call: query your database, run a report, send an email, perform a calculation
  • Expose resources — data the AI can read: configuration, documents, live metrics
  • Expose prompts — reusable conversation templates that guide the AI’s behavior

The result? You can ask your AI assistant: “Which product generated the most revenue in March?” — and the answer comes directly from your ERP, your database, your Delphi application. The AI doesn’t guess; it calls your tool, gets the real data, and presents it.

MCP Server for DMVCFramework is a production-ready implementation of this protocol for the Delphi ecosystem. It leverages DMVCFramework’s battle-tested JSON-RPC infrastructure and adds an attribute-driven, RTTI-based discovery system that makes building an MCP server almost trivial.


More Than a Library: Server, Client, and Agent

Most “MCP for X” projects stop at the server: they let you build something the AI calls into. This project is different — it gives Delphi all three corners of the MCP world, plus a bridge for existing apps:

Role Class / Unit What it does
🟢 Server MVCFramework.MCP.Server Expose your Delphi tools, resources, and prompts to AI assistants over HTTP or stdio. Attribute-driven, zero boilerplate.
🔵 Client TMCPClient / TMCPStdioClient Consume any spec-compliant MCP server from your own Delphi code — over Streamable HTTP, or by spawning a stdio server as a child process.
🟣 Agent TMCPOpenAIAgent A complete agent loop wiring an OpenAI-compatible LLM to an MCP server: tool discovery → LLM round-trips → tool dispatch → token accounting.
🌉 REST → MCP Bridge MVCFramework.MCP.Bridge Auto-expose an existing DMVCFramework REST API as MCP tools by scanning the engine’s routes via RTTI — no tools written by hand.

This means you can build AI-powered Delphi applications where Delphi is the server, the client, and the agent. The Writing the Client and Building an Agent sections below cover the client and agent halves in detail.


Key Features

Feature Description
MCP 2025-03-26 compliant Implements the latest version of the protocol specification
Server, client AND agent Build servers, consume them with TMCPClient / TMCPStdioClient, and drive them from an LLM with TMCPOpenAIAgent
REST → MCP bridge Turn an existing DMVCFramework REST API into MCP tools automatically (RTTI route scanning)
Attribute-driven Decorate methods with [MCPTool], [MCPResource], [MCPPrompt] — no boilerplate
Type-safe parameters string, Integer, Double, Boolean — auto-generates JSON Schema
Dual transport Streamable HTTP and stdio, on both the server and client side
Session management Thread-safe in-memory sessions with automatic 30-minute timeout
Rich content types Text, Image (base64), Audio (base64), Embedded Resources
Fluent API Chain .AddText(), .AddImage(), .AddResource() for multi-content responses
Delphi serialization FromObject(), FromCollection(), FromDataSet(), FromRecord() — serialize any Delphi type
DMVCFramework integration Single PublishObject call to add MCP to any existing DMVCFramework server
Apache 2.0 Free for commercial and personal use

Getting Started

The fastest way to start is to copy a Quick Start sample from the repository and customize it.

Prerequisites

  • Delphi 11+ (Alexandria or later)
  • DMVCFramework 3.5.x installed and in your library path
  • The sources/ folder from the repository in your project’s search path

Choose Your Transport

Three ready-to-run Quick Start projects are included:

Project Role Transport TaurusTLS Use when
quickstart/quickstart/ Server HTTP + stdio Required You want a network server that AI clients connect to remotely
quickstart/quickstart_stdio/ Server stdio only Not needed You want the AI client (e.g. Claude Desktop) to launch the server locally
quickstart/quickstart_stdio_agent/ Agent (host + client) stdio Not needed You want a Delphi-side AI agent that spawns and consumes a stdio MCP server, driven by an LLM

All projects share the same provider units in quickstart/shared/ — you write your tools, resources, and prompts once and every project uses them automatically.

Project Structure

quickstart/
├── shared/                      <-- YOUR CODE: customize these files
│   ├── ToolProviderU.pas        <--   tools the AI can call
│   ├── ResourceProviderU.pas    <--   data the AI can read
│   └── PromptProviderU.pas      <--   conversation templates
│
├── quickstart/                  <-- HTTP + stdio server (Indy Direct)
│   ├── QuickStart.dpr/.dproj
│   └── bin/.env
│
├── quickstart_stdio/            <-- stdio-only server (no TaurusTLS)
│   └── QuickStartStdio.dpr/.dproj
│
└── quickstart_stdio_agent/      <-- Delphi-side AI agent (spawns + drives a stdio server)
    ├── QuickStartStdioAgent.dpr/.dproj
    └── bin/.env.example          <-- LLM key + model + server command

Copy quickstart/shared/ plus the project folder you need, open the .dproj in Delphi, build, and run.

Download from GitHub


Writing Tools

Tools are the heart of an MCP server. They are functions that AI assistants can call to perform actions on your behalf.

Your First Tool

Create a class that extends TMCPToolProvider, decorate methods with [MCPTool], and annotate parameters with [MCPParam]:

type
  TMyTools = class(TMCPToolProvider)
  public
    [MCPTool('reverse_string', 'Reverses a string')]
    function ReverseString(
      [MCPParam('The string to reverse')] const Value: string
    ): TMCPToolResult;
  end;

function TMyTools.ReverseString(const Value: string): TMCPToolResult;
begin
  Result := TMCPToolResult.Text(System.StrUtils.ReverseString(Value));
end;

initialization
  TMCPServer.Instance.RegisterToolProvider(TMyTools);

That’s it. The library:

  1. Discovers ReverseString via RTTI
  2. Reads the [MCPTool] attribute to get the name and description
  3. Reads the [MCPParam] attribute and the Delphi method signature to generate the JSON Schema
  4. Makes the tool available to any AI client that connects

Supported Parameter Types

Delphi Type MCP JSON Schema Type Example
string string const Name: string
Integer integer const Count: Integer
Int64 integer const BigNum: Int64
Double number const Price: Double
Boolean boolean const Active: Boolean

Optional Parameters

By default, parameters are required. To make a parameter optional, pass False as the second argument to MCPParam:

[MCPTool('greet', 'Greets a user')]
function Greet(
  [MCPParam('User name')] const Name: string;
  [MCPParam('Greeting style', False)] const Style: string  // optional
): TMCPToolResult;

When the AI doesn’t provide an optional parameter, Delphi receives the default value for that type (empty string, 0, 0.0, False).

Returning Results

The TMCPToolResult record offers multiple factory methods depending on what you need to return:

Simple text

Result := TMCPToolResult.Text('Hello, world!');

Error (the AI sees isError=true)

if B = 0 then
  Result := TMCPToolResult.Error('Division by zero is not allowed');

Scalar values (Integer, Double, Boolean)

Result := TMCPToolResult.FromValue(A + B);       // Double
Result := TMCPToolResult.FromValue(42);           // Integer
Result := TMCPToolResult.FromValue(True);         // Boolean

JSON object

LJSON := TJDOJsonObject.Create;
try
  LJSON.S['status'] := 'healthy';
  LJSON.I['uptime'] := 3600;
  Result := TMCPToolResult.JSON(LJSON);
finally
  LJSON.Free;
end;

Serialized Delphi object

LUser := TUser.Create;
try
  LUser.Name := 'Alice';
  LUser.Email := 'alice@example.com';
  Result := TMCPToolResult.FromObject(LUser);  // auto-serializes via DMVCFramework
finally
  LUser.Free;
end;

Collection (TObjectList)

Result := TMCPToolResult.FromCollection(LPersonList);

Dataset

Result := TMCPToolResult.FromDataSet(LQuery);

Image or audio

Result := TMCPToolResult.Image(LBase64Data, 'image/png');
Result := TMCPToolResult.Audio(LBase64Data, 'audio/wav');

Multiple content items (Fluent API)

Result := TMCPToolResult.Text('Analysis complete')
  .AddImage(LChartBase64, 'image/png')
  .AddResource('file:///report.csv', LCsvData, 'text/csv');

Complete Factory Methods Reference

Method Returns
TMCPToolResult.Text(AText) Text content
TMCPToolResult.Error(AMessage) Error text with isError=true
TMCPToolResult.Image(ABase64, AMimeType) Image content
TMCPToolResult.Audio(ABase64, AMimeType) Audio content
TMCPToolResult.Resource(AURI, AText, AMimeType) Embedded resource (text)
TMCPToolResult.ResourceBlob(AURI, ABase64, AMimeType) Embedded resource (binary)
TMCPToolResult.JSON(AJsonObject) Serialized JSON
TMCPToolResult.FromObject(AObject) Serialized TObject
TMCPToolResult.FromCollection(AList) Serialized TObjectList
TMCPToolResult.FromRecord(ARecord, ATypeInfo) Serialized record
TMCPToolResult.FromDataSet(ADataSet) Serialized TDataSet
TMCPToolResult.FromValue(AValue) Integer, Int64, Double, or Boolean as text
TMCPToolResult.FromStream(AStream, AMimeType) Base64-encoded stream

Writing Resources

Resources are data that AI assistants can read. Each resource is identified by a URI and has a MIME type.

type
  TMyResources = class(TMCPResourceProvider)
  public
    [MCPResource('config://app/settings', 'Application Settings',
      'Returns the current app configuration', 'application/json')]
    function GetSettings(const URI: string): TMCPResourceResult;

    [MCPResource('file:///assets/logo.png', 'Logo',
      'Returns the application logo', 'image/png')]
    function GetLogo(const URI: string): TMCPResourceResult;
  end;

function TMyResources.GetSettings(const URI: string): TMCPResourceResult;
begin
  Result := TMCPResourceResult.Text(URI,
    '{"appName": "MyApp", "version": "1.0.0"}',
    'application/json');
end;

function TMyResources.GetLogo(const URI: string): TMCPResourceResult;
begin
  Result := TMCPResourceResult.Blob(URI, LBase64Data, 'image/png');
end;

initialization
  TMCPServer.Instance.RegisterResourceProvider(TMyResources);

The MCPResource Attribute

[MCPResource(URI, Name, Description, MimeType)]
Parameter Description
URI The resource identifier (e.g., config://app/settings, file:///docs/readme.txt)
Name Human-readable name displayed to the AI
Description What this resource contains
MimeType Content type (text/plain, application/json, image/png, etc.)

Result Factory Methods

Method Use for
TMCPResourceResult.Text(URI, Content, MimeType) Text-based content (JSON, plain text, CSV, XML)
TMCPResourceResult.Blob(URI, Base64Data, MimeType) Binary content (images, PDFs, archives)

Writing Prompts

Prompts are reusable conversation templates. When an AI client requests a prompt, your server returns a pre-built conversation that the AI uses as context.

type
  TMyPrompts = class(TMCPPromptProvider)
  public
    [MCPPrompt('code_review', 'Generates a code review prompt')]
    [MCPPromptArg('code', 'The source code to review', True)]       // required
    [MCPPromptArg('language', 'Programming language', False)]        // optional
    function CodeReview(const Arguments: TJDOJsonObject): TMCPPromptResult;
  end;

function TMyPrompts.CodeReview(const Arguments: TJDOJsonObject): TMCPPromptResult;
var
  LCode, LLang: string;
begin
  LCode := Arguments.S['code'];
  LLang := Arguments.S['language'];
  if LLang.IsEmpty then
    LLang := 'unknown language';

  Result := TMCPPromptResult.Create(
    'Code review for ' + LLang,
    [
      PromptMessage('user',
        'Please review the following ' + LLang + ' code:' +
        sLineBreak + sLineBreak + LCode),
      PromptMessage('assistant',
        'I will review the code focusing on correctness and best practices.')
    ]);
end;

initialization
  TMCPServer.Instance.RegisterPromptProvider(TMyPrompts);

Prompt Attributes

Attribute Purpose
[MCPPrompt('name', 'description')] Marks a method as an MCP prompt
[MCPPromptArg('name', 'description', Required)] Declares an argument. True = required, False = optional

Message Types

Function Creates
PromptMessage('user', 'text...') A text message with role user or assistant
PromptImageMessage('user', Base64Data, MimeType) A message containing an image
PromptResourceMessage('user', URI, Text, MimeType) A message containing an embedded resource

The Registration Pattern

Every provider follows the same pattern: create a class, decorate methods with attributes, register in the initialization section:

initialization
  TMCPServer.Instance.RegisterToolProvider(TMyTools);
  TMCPServer.Instance.RegisterResourceProvider(TMyResources);
  TMCPServer.Instance.RegisterPromptProvider(TMyPrompts);

At startup, TMCPServer scans each provider class via RTTI, discovers all decorated methods, builds the JSON Schema for parameters, and makes everything available to AI clients. There is no manual wiring, no configuration file, no XML — just Delphi code and attributes.

You can split providers across multiple units and multiple classes. This is useful for organizing large projects:

// OrderToolsU.pas
initialization
  TMCPServer.Instance.RegisterToolProvider(TOrderTools);

// InventoryToolsU.pas
initialization
  TMCPServer.Instance.RegisterToolProvider(TInventoryTools);

// ReportToolsU.pas
initialization
  TMCPServer.Instance.RegisterToolProvider(TReportTools);

Connecting AI Clients

Claude Desktop

Edit %APPDATA%\Claude\claude_desktop_config.json (Windows) or ~/Library/Application Support/Claude/claude_desktop_config.json (macOS):

Streamable HTTP (your server is already running on the network):

{
  "mcpServers": {
    "my-erp": {
      "url": "http://your-server:8080/mcp"
    }
  }
}

stdio (Claude Desktop launches your executable directly):

{
  "mcpServers": {
    "my-erp": {
      "command": "C:\\path\\to\\QuickStartStdio.exe"
    }
  }
}

Claude Code (CLI)

claude mcp add --transport http my-erp http://your-server:8080/mcp

Google Gemini CLI

Edit ~/.gemini/settings.json:

{
  "mcpServers": {
    "my-erp": {
      "url": "http://your-server:8080/mcp"
    }
  }
}

Continue (VS Code / JetBrains)

Edit ~/.continue/config.yaml:

mcpServers:
  - name: my-erp
    url: http://your-server:8080/mcp

Any MCP-compatible Client

Any client that supports MCP Streamable HTTP:

POST http://your-server:8080/mcp
Content-Type: application/json

{"jsonrpc":"2.0","method":"initialize","params":{...},"id":1}

Consuming MCP Servers: The Client

So far we’ve made Delphi the server. But the library works just as well in the other direction: your Delphi application can be the MCP client, calling tools on any spec-compliant MCP server — whether it’s this library’s own server, a server written in Python or TypeScript, or any third-party MCP server.

There are two client classes, both sharing the same public surface through a common TMCPClientBase ancestor, so you can swap transports without touching your logic:

Class Unit Transport
TMCPClient MVCFramework.MCP.Client Streamable HTTP (one POST per JSON-RPC call)
TMCPStdioClient MVCFramework.MCP.Client.Stdio stdio — spawns the server as a child process and talks over its stdin/stdout pipes

HTTP client

uses
  System.JSON,
  MVCFramework.MCP.Client;

var
  LClient: TMCPClient;
  LTools: TJSONArray;
  LArgs: TJSONObject;
begin
  LClient := TMCPClient.Create('http://localhost:8080/mcp');
  try
    LClient.Initialize;                       // MCP handshake

    // Discover what the server offers
    LTools := LClient.ListTools;              // ownership transferred to you
    try
      WriteLn(Format('Server exposes %d tool(s)', [LTools.Count]));
    finally
      LTools.Free;
    end;

    // Call a tool. The arguments object is CONSUMED by CallTool.
    LArgs := TJSONObject.Create;
    LArgs.AddPair('Value', 'Hello, MCP!');
    WriteLn(LClient.CallTool('reverse_string', LArgs));
  finally
    LClient.Free;
  end;
end;

stdio client

The stdio client spawns the server executable itself and communicates over pipes — exactly the way Claude Desktop launches a local stdio server, but here you are the host:

uses
  MVCFramework.MCP.Client.Stdio;

var
  LClient: TMCPStdioClient;
begin
  LClient := TMCPStdioClient.Create('C:\path\to\QuickStartStdio.exe');
  try
    LClient.Initialize;
    WriteLn(LClient.CallTool('reverse_string',
      TJSONObject.Create.AddPair('Value', 'Hello over a pipe')));
  finally
    LClient.Free;   // closes stdin (EOF), waits for graceful exit, then terminates if needed
  end;
end;

Client API

Both clients expose the full MCP surface:

Method Returns
Initialize Performs the initialize + notifications/initialized handshake
ListTools Array of tool descriptors {name, description, inputSchema}
CallTool(name, args) The concatenated text of every content block the tool returns
ListResources Array of static resource descriptors
ListResourceTemplates Array of templated (URI) resource descriptors
ReadResource(uri, out mimeType) The resource content as text (blobs reported as a placeholder)
ListPrompts Array of prompt descriptors
GetPrompt(name, args) The rendered messages array, ready to forward to an LLM

Memory convention: methods that take a TJSONObject argument consume it (free it for you); methods that return a TJSONObject / TJSONArray transfer ownership to you — you free the result. This mirrors TJSONObject.AddPair semantics and keeps the ownership story trivial.


Building an Agent: TMCPOpenAIAgent

A client lets you call tools by hand. An agent lets an LLM decide which tools to call, in what order, with what arguments — and loops until it has an answer. TMCPOpenAIAgent (in MVCFramework.MCP.OpenAIAgent) is exactly that loop, in a single, dependency-light class.

What an agent actually is

Strip away the buzzword and an agent is just a loop:

  1. Build the message list: [system, user, ...]
  2. Call the LLM with (messages, tools)
  3. The LLM responds:
    • finish_reason = "stop" → we have the final answer, exit
    • finish_reason = "tool_calls" → the LLM wants to invoke tools
  4. For each tool call: execute it via the MCP server, append the result as a role:"tool" message
  5. Go back to step 2

TMCPOpenAIAgent runs this loop for you, bridges MCP tool descriptors into OpenAI function-calling format automatically, accumulates token usage, and caps the number of round-trips with MaxTurns as a safety net.

Works with any OpenAI-compatible LLM

Despite the “OpenAI” in the name, the class speaks the OpenAI chat-completions wire format — not the OpenAI vendor. It works against anything that speaks that format:

  • OpenAI (https://api.openai.com/v1)
  • OpenRouter (https://openrouter.ai/api/v1) — including the optional HTTP-Referer / X-Title analytics headers
  • Anthropic via an OpenAI-compatible proxy
  • Together, Groq, vLLM
  • Local models: Ollama (http://localhost:11434/v1), llama.cpp (--api)

The class is named for the wire format, not the brand.

A minimal agent

uses
  System.JSON,
  MVCFramework.MCP.OpenAIAgent;

var
  LAgent: TMCPOpenAIAgent;
  LMessages: TJSONArray;
  LMsg: TJSONObject;
  LResult: TMCPAgentResult;
begin
  // (MCP server URL, LLM API key, model, [LLM base URL])
  LAgent := TMCPOpenAIAgent.Create(
    'http://localhost:8080/mcp',
    'sk-...your-key...',
    'gpt-4o-mini');
  try
    LAgent.SystemPrompt := 'You are a helpful Delphi-powered assistant. ' +
      'Always call the appropriate MCP tool before answering about live data.';
    LAgent.MaxTurns := 25;

    // The conversation history — typically a single user message.
    LMessages := TJSONArray.Create;
    try
      LMsg := TJSONObject.Create;
      LMsg.AddPair('role', 'user');
      LMsg.AddPair('content', 'Reverse the string "DMVCFramework" for me.');
      LMessages.AddElement(LMsg);

      LResult := LAgent.Run(LMessages);   // discovers tools, loops, dispatches

      WriteLn(LResult.Content);
      WriteLn(Format('(%d tool calls, %d tokens)',
        [LResult.ToolCallCount, LResult.TotalTokens]));
    finally
      LMessages.Free;
    end;
  finally
    LAgent.Free;
  end;
end;

Run returns a TMCPAgentResult record with the final text plus running cost:

TMCPAgentResult = record
  Content: string;            // final text from the assistant
  PromptTokens: Integer;      // sum across all turns
  CompletionTokens: Integer;  // sum across all turns
  TotalTokens: Integer;       // Prompt + Completion
  ToolCallCount: Integer;     // total MCP tool calls executed during the run
end;

Where the agent earns its keep: multi-step tasks

A single-shot question — “which product generated the most revenue in March?” — doesn’t need an agent at all. That’s one tool call; a plain TMCPClient.CallTool would answer it. The agent earns its keep when a request takes several tool calls and reasoning in between, planned on the fly by the LLM.

Suppose your ERP exposes three tools — get_top_products, get_customer_balance, and create_invoice (the very tools from the Real-World Example below). Now ask:

“Customer C-1024 wants to reorder last March’s best-selling product. If they’re still within their credit limit, create a draft invoice for 50 units.”

No single tool answers that. The agent works it out by itself, turn after turn:

user      "Customer C-1024 wants to reorder last March's best-selling
           product. If they're within their credit limit, create a
           draft invoice for 50 units."

turn 1    LLM → call get_top_products(month=3, year=2026, topN=1)
          MCP → "Industrial Pump X200 — €182,400 revenue"

turn 2    LLM → call get_customer_balance(customerCode="C-1024")
          MCP → { "balance": 12300, "creditLimit": 50000 }

turn 3    LLM → reasons: 12,300 + 50 × unit price is still under 50,000 ✓
          LLM → call create_invoice(customerCode="C-1024",
                  description="50× Industrial Pump X200", amount=…)
          MCP → "Draft invoice INV-2026-0337 created"

turn 4    LLM → finish_reason "stop"
          "Done. C-1024 is within their €50,000 credit limit, so I created
           draft invoice INV-2026-0337 for 50 units of the Industrial Pump
           X200 — March's best-selling product."

Three tools, chained across four turns, with a credit-limit check in the middle — and you wrote none of that orchestration. You declared the tools; the LLM planned the sequence; TMCPOpenAIAgent.Run drove the loop and handed you back the final text plus ToolCallCount = 3 and the token totals. The same call with a different question might use one tool, or seven — the plan is decided at runtime, not by you.

That is the difference between calling a tool and running an agent.

Pointing at a different provider

Just override the base URL (4th constructor argument) and the model:

// OpenRouter
LAgent := TMCPOpenAIAgent.Create(LMcpUrl, LKey,
  'anthropic/claude-3.5-sonnet', 'https://openrouter.ai/api/v1');
LAgent.HTTPReferer := 'https://myapp.example';   // optional OpenRouter analytics
LAgent.XTitle      := 'My Delphi Agent';

// Local Ollama — no key needed
LAgent := TMCPOpenAIAgent.Create(LMcpUrl, 'ollama',
  'llama3.1', 'http://localhost:11434/v1');

Driving a stdio server instead of HTTP

By default Run creates a fresh HTTP TMCPClient from the URL you passed. To drive a stdio server instead — spawning it as a child process, the way the bundled quickstart_stdio_agent sample does — inject a TMCPStdioClient before calling Run:

LStdio := TMCPStdioClient.Create('C:\path\to\QuickStartStdio.exe');
LStdio.Initialize;

LAgent := TMCPOpenAIAgent.Create('', LKey, 'gpt-4o-mini');   // URL unused
LAgent.SetMCPClient(LStdio, {AOwns:} False);                 // inject the transport
LAgent.SystemPrompt := '...';
LResult := LAgent.Run(LMessages);

This is the “host + client” pattern: your Delphi program spawns and consumes a local MCP server and drives it with an LLM — a self-contained AI agent, entirely in Object Pascal. The complete, runnable version (with a small terminal UI, spinner, and markdown rendering) ships as the quickstart_stdio_agent project.


Architecture

The library is structured in layers with a clean separation between transport and protocol logic:

┌─────────────────────────────────────────────────────────────┐
│                      Client (AI Assistant)                  │
└──────────┬─────────────────────────────────┬────────────────┘
           │ Streamable HTTP                  │ stdio
           ▼                                  ▼
┌──────────────────────────────┐  ┌──────────────────────────┐
│  TMCPEndpoint (PublishObject)│  │  TMCPStdioTransport      │
│  TMCPSessionController      │  │  (stdin/stdout JSON-RPC)  │
│  (POST/DELETE /mcp)         │  │                           │
└──────────────┬──────────────┘  └─────────────┬────────────┘
               │                                │
               └──────────┬─────────────────────┘
                          ▼
┌─────────────────────────────────────────────────────────────┐
│  TMCPRequestHandler (transport-agnostic dispatch)           │
└──────────────────────────┬──────────────────────────────────┘
                           ▼
┌─────────────────────────────────────────────────────────────┐
│  TMCPServer (Singleton)                                     │
│  ├─ Tool Registry                                           │
│  ├─ Resource Registry                                       │
│  ├─ Prompt Registry                                         │
│  └─ Session Manager                                         │
└─────────────────────────────────────────────────────────────┘
                           │
               ┌───────────┼───────────┐
               ▼           ▼           ▼
         TMCPTool    TMCPResource  TMCPPrompt
         Provider    Provider       Provider

Key classes:

Class File Role
TMCPServer MVCFramework.MCP.Server.pas Singleton registry. Scans providers via RTTI, manages sessions
TMCPRequestHandler MVCFramework.MCP.RequestHandler.pas Transport-agnostic protocol dispatch. Handles all MCP methods
TMCPEndpoint MVCFramework.MCP.Server.pas Published object for HTTP transport. Created per request
TMCPStdioTransport MVCFramework.MCP.Stdio.pas Reads stdin, writes stdout. Zero HTTP dependencies
TMCPSessionController MVCFramework.MCP.Server.pas Handles HTTP DELETE for session termination
TMCPClient MVCFramework.MCP.Client.pas Client — consume an MCP server over Streamable HTTP
TMCPStdioClient MVCFramework.MCP.Client.Stdio.pas Client — consume a stdio MCP server (spawns it as a child process)
TMCPOpenAIAgent MVCFramework.MCP.OpenAIAgent.pas Agent — LLM-driven loop over an MCP server (any transport)
TMCPBridgeProvider MVCFramework.MCP.Bridge.pas Bridge — exposes existing DMVCFramework REST routes as MCP tools

Adding MCP to an Existing DMVCFramework Application

If you already have a DMVCFramework application and want to add MCP capabilities, it’s just a few lines in your Web Module:

uses
  MVCFramework.MCP.Server;

procedure TMyWebModule.WebModuleCreate(Sender: TObject);
begin
  fMVC := TMVCEngine.Create(Self, ...);

  // Add these two lines:
  fMVC.AddController(TMCPSessionController);
  fMVC.PublishObject(
    function: TObject
    begin
      Result := TMCPServer.Instance.CreatePublishedEndpoint;
    end, '/mcp');

  // ... your existing controllers, middleware, etc.
end;

Then create your provider units, add them to the .dpr uses clause, and your existing application now speaks MCP. Your existing REST endpoints, WebSocket handlers, and everything else continue to work exactly as before — MCP is just an additional endpoint.


Real-World Example: Exposing an ERP

Here’s what a real-world tool provider might look like for an ERP system:

type
  TERPTools = class(TMCPToolProvider)
  public
    [MCPTool('get_top_products', 'Returns the top N products by revenue for a given month')]
    function GetTopProducts(
      [MCPParam('Month number (1-12)')] const Month: Integer;
      [MCPParam('Year')] const Year: Integer;
      [MCPParam('Number of products to return', False)] const TopN: Integer
    ): TMCPToolResult;

    [MCPTool('get_customer_balance', 'Returns the current balance for a customer')]
    function GetCustomerBalance(
      [MCPParam('Customer code')] const CustomerCode: string
    ): TMCPToolResult;

    [MCPTool('create_invoice', 'Creates a draft invoice for a customer')]
    function CreateInvoice(
      [MCPParam('Customer code')] const CustomerCode: string;
      [MCPParam('Invoice description')] const Description: string;
      [MCPParam('Total amount')] const Amount: Double
    ): TMCPToolResult;
  end;

function TERPTools.GetTopProducts(const Month, Year, TopN: Integer): TMCPToolResult;
var
  LQuery: TFDQuery;
  LActualTopN: Integer;
begin
  LActualTopN := TopN;
  if LActualTopN <= 0 then
    LActualTopN := 10;

  LQuery := TFDQuery.Create(nil);
  try
    LQuery.Connection := GetERPConnection;
    LQuery.SQL.Text :=
      'SELECT TOP :topn p.ProductName, SUM(oi.Quantity * oi.UnitPrice) as Revenue ' +
      'FROM OrderItems oi ' +
      'JOIN Products p ON oi.ProductID = p.ProductID ' +
      'JOIN Orders o ON oi.OrderID = o.OrderID ' +
      'WHERE MONTH(o.OrderDate) = :month AND YEAR(o.OrderDate) = :year ' +
      'GROUP BY p.ProductName ' +
      'ORDER BY Revenue DESC';
    LQuery.ParamByName('topn').AsInteger := LActualTopN;
    LQuery.ParamByName('month').AsInteger := Month;
    LQuery.ParamByName('year').AsInteger := Year;
    LQuery.Open;

    // FromDataSet serializes the entire result set to a JSON array
    Result := TMCPToolResult.FromDataSet(LQuery);
  finally
    LQuery.Free;
  end;
end;

Now you can ask Claude: “Which products generated the most revenue in March 2026?” — and it will call your get_top_products tool, get real data from your database, and present it in a natural language answer.


MCP Protocol Methods

The server implements all required MCP 2025-03-26 protocol methods:

Method Description
initialize Creates a session, returns server capabilities
notifications/initialized Client notification (no response)
ping Health check
tools/list Lists all available tools with their JSON schemas
tools/call Executes a tool by name with arguments
resources/list Lists all available resources
resources/read Reads a resource by URI
prompts/list Lists all available prompts with their argument schemas
prompts/get Gets a prompt with filled-in arguments

Configuration

Create a .env file in your application directory to configure the HTTP server:

# Server port
dmvc.server.port=8080

# HTTPS (optional)
https.enabled=true
https.cert.cacert=certificates\localhost.crt
https.cert.privkey=certificates\localhost.key
https.cert.password=

Testing

The library is covered by four independent compliance suites spanning the server, the client, and the agent:

  • Python HTTP suite (tests/test_mcp_server.py) — 185 cases exercising the server end-to-end over Streamable HTTP.
  • Python stdio suite (tests/test_mcp_server_stdio.py) — 142 cases exercising the stdio transport, including verification that stdout carries only valid JSON-RPC (no log pollution).
  • TMCPClient suite (tests/clientproject/) — 21 Delphi cases that drive the Delphi client against the running server. The same test code runs twice: once over HTTP (TMCPClient) and once over stdio (TMCPStdioClient).
  • TMCPOpenAIAgent suite (tests/agentproject/) — 8 Delphi cases exercising the agent loop against a deterministic embedded fake LLM, so the loop is validated without any external network dependency.

Together they cover:

  • MCP 2025-03-26 protocol compliance and JSON-RPC 2.0
  • Session lifecycle (create, validate, delete, timeout) and concurrent sessions
  • Tool/Resource/Prompt execution and error handling, on both server and client side
  • All content types (text, image, audio, embedded resources) and URI templates
  • The full agent loop: tool dispatch, token accounting, system prompt, MaxTurns safety net

Frequently Asked Questions (FAQ)

Is this free for commercial use?

Yes. MCP Server for DMVCFramework is released under the Apache 2.0 License — free for commercial and personal use, with no restrictions.

Which Delphi versions are supported?

Delphi 11 (Alexandria) and later. The library uses modern language features like custom attributes and enhanced RTTI.

Can I use this without DMVCFramework?

The HTTP transport requires DMVCFramework. However, the stdio transport has minimal dependencies — it could theoretically be adapted for other frameworks, though this is not currently a supported configuration.

Can my Delphi application act as an MCP client, not just a server?

Yes — that’s a first-class feature. Use TMCPClient to talk to any MCP server over Streamable HTTP, or TMCPStdioClient to spawn a stdio server as a child process and talk to it over pipes. Both share the same API, so you can switch transport without changing your code. See Consuming MCP Servers: The Client.

Can I build an AI agent in Delphi with this?

Yes. TMCPOpenAIAgent is a complete agent loop: it discovers the server’s tools, sends them to an LLM in function-calling format, executes whatever tools the LLM asks for, feeds the results back, and repeats until the LLM produces a final answer — accumulating token usage along the way. It works with any OpenAI-compatible LLM, including local ones (Ollama, llama.cpp). See Building an Agent.

Can I make an existing DMVCFramework REST API available to AI without rewriting it?

Yes. The MVCFramework.MCP.Bridge unit scans your engine’s routes via RTTI and exposes them as MCP tools automatically — path/query/body parameters become tool parameters with the right JSON Schema types. Your existing REST API becomes AI-ready without writing a single tool by hand.

Can I expose both MCP and REST endpoints in the same application?

Absolutely. MCP is published as an additional endpoint (/mcp) alongside your existing REST controllers, WebSocket handlers, and anything else DMVCFramework supports. They coexist without interference.

How do sessions work?

When an AI client sends an initialize request, the server creates a session and returns a session ID in the Mcp-Session-Id header. The client includes this header in subsequent requests. Sessions expire automatically after 30 minutes of inactivity and can be terminated early via HTTP DELETE.

What happens if a tool raises an exception?

Unhandled exceptions in tool methods are caught by the library and returned as JSON-RPC error responses. For expected error conditions, use TMCPToolResult.Error('message') instead — this returns a proper MCP error result with isError=true, which is more informative for the AI.


Comments

comments powered by Disqus