Become a member!

MCP Server for DMVCFramework - Build AI-Ready Delphi Applications

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 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.

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}

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