MCP for Delphi — Server, Client & Agent for DMVCFramework
Let your Delphi applications talk to AI assistants
MCP Protocol 2025-03-26 | GitHub Repository | Apache 2.0 License
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/TMCPStdioClientto 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.
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:
- Discovers
ReverseStringvia RTTI - Reads the
[MCPTool]attribute to get the name and description - Reads the
[MCPParam]attribute and the Delphi method signature to generate the JSON Schema - 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
TJSONObjectargument consume it (free it for you); methods that return aTJSONObject/TJSONArraytransfer ownership to you — you free the result. This mirrorsTJSONObject.AddPairsemantics 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:
- Build the message list:
[system, user, ...] - Call the LLM with
(messages, tools) - The LLM responds:
finish_reason = "stop"→ we have the final answer, exitfinish_reason = "tool_calls"→ the LLM wants to invoke tools
- For each tool call: execute it via the MCP server, append the result as a
role:"tool"message - 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 optionalHTTP-Referer/X-Titleanalytics 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). TMCPClientsuite (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).TMCPOpenAIAgentsuite (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,
MaxTurnssafety 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