MCP Server for DMVCFramework - Build AI-Ready Delphi Applications
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 lets you expose your Delphi business logic to AI assistants like Claude, Gemini, and ChatGPT. Decorate methods with
[MCPTool],[MCPResource], or[MCPPrompt]attributes and the library does the rest — JSON schema generation, session management, JSON-RPC dispatch, dual transport (HTTP + stdio). Copy the Quick Start sample, 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.
Key Features
| Feature | Description |
|---|---|
| MCP 2025-03-26 compliant | Implements the latest version of the protocol specification |
| 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, selected via --transport http|stdio |
| 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
Two ready-to-run Quick Start projects are included:
| Project | Transport | TaurusTLS | Use when |
|---|---|---|---|
samples/quickstart/ |
HTTP + stdio | Required | You want a network server that AI clients connect to remotely |
samples/quickstart_stdio/ |
stdio only | Not needed | You want the AI client (e.g. Claude Desktop) to launch the server locally |
Both projects share the same provider units in samples/shared/ — you write your tools, resources, and prompts once and both transports use them automatically.
Project Structure
samples/
├── 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 project
│ ├── QuickStart.dpr/.dproj
│ ├── WebModuleU.pas/.dfm
│ └── bin/.env
│
└── quickstart_stdio/ <-- stdio-only project (no TaurusTLS)
└── QuickStartStdio.dpr/.dproj
Copy samples/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}
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 |
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 repository includes a comprehensive Python test suite (tests/test_mcp_server.py) with 20+ test cases covering:
- MCP 2025-03-26 protocol compliance
- JSON-RPC 2.0 specification
- Session lifecycle (create, validate, delete, timeout)
- Tool/Resource/Prompt execution and error handling
- All content types (text, image, audio, embedded resources)
- Concurrent sessions
- HTTP method restrictions
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 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