MCP resources and prompts in MARS


In the previous post I introduced MCP support in MARS-Curiosity: expose Delphi methods and FireDAC data as tools that AI agents like Claude can discover and call, secured by roles and OAuth 2.1. Tools are the headline feature of MCP — but the protocol defines two more capabilities, and MARS now implements the full triad.

The neat way to understand them is to ask who is in control:

  • Tools — the model decides to call them while reasoning;
  • Resources — readable content identified by a URI, that the client or user attaches to the conversation;
  • Prompts — reusable, parameterized templates the user picks from the client UI, slash-command style.

Resources: what the agent should know, not fetch

Some information should not cost a tool call: the agent should simply have it. The classic example is your database schema — without it, the model wastes an exploratory call to learn table and column names. With MARS, a resource is just another annotated method:

// static resource, listed in resources/list
[MCPResource('db://schema', 'schema', 'DDL of the database: read this to learn tables and columns', 'text/plain')]
function DbSchema: string;

// URI template, listed in resources/templates/list: {id} binds to the parameter
[MCPResource('employees://{id}', 'employee', 'A single employee record by numeric id')]
function EmployeeResource([MCPParam('id', 'Employee id')] const AId: Integer): TFDQuery;

The first returns the real DDL (the demo reads it from sqlite_master); the second is a URI template: a client asking for employees://3 gets the URI matched, the 3 extracted and coerced to Integer, the parametrized FireDAC query executed and the row serialized as JSON — all by the library. Serialization follows the return type: strings become plain text, records and datasets become JSON, a TStream becomes a base64 blob. In Claude, resources show up in the attach menu; in Claude Code you can reference them inline with @server:db://schema.

Prompts: ship the workflow, not just the tools

You built the tools, so you know how they are meant to be used. A prompt lets you encode that knowledge on the server, and users invoke it like a slash command. The demo ships a guided salary review:

[MCPPrompt('salary_review', 'Guided salary review for an employee')]
function SalaryReviewPrompt(
  [MCPParam('employeeName', 'Employee to review')] const AName: string): string;

function TDemoDBMCPResource.SalaryReviewPrompt(const AName: string): string;
begin
  Result :=
    'Perform a salary review for the employee matching "' + AName + '":' + sLineBreak
    + '1. Use the find_employees tool to locate the employee.' + sLineBreak
    + '2. Use list_employees to compare with colleagues in similar roles.' + sLineBreak
    + '3. Propose a fair adjustment (as a percentage) with a short motivation.' + sLineBreak
    + '4. Only if I explicitly confirm, apply it with the raise_salary tool.';
end;

I ran exactly this from Claude: the agent located the employee, compared salaries across the team, proposed a motivated adjustment — and then stopped and asked for confirmation, because step 4 says so. Guardrails for destructive actions, defined server-side by the people who know the domain: that is what prompts are for.

Authorization covers everything

The role-based filtering introduced for tools applies unchanged to resources and prompts: put [RolesAllowed('admin')] on a resource method and non-admin agents will not see it in any list — reading it answers Resource not found, with no hint it exists. One security model, three capabilities, zero extra code. The initialize response advertises resources and prompts only when your class actually declares some.

Quality of life

Two smaller additions born from real-world testing sessions:

  • Console logging in the demo — one line per request with timing and the JSON-RPC detail (tools/call add_numbers, resources/read employees://1…). This enables watching an agent work against your server in real time (nice for demos).
  • OAuth store persistence — one call to TMCPOAuthServer.SetPersistenceFile(...) and registered clients plus refresh tokens survive server restarts. Without it, every restart answered “Unknown client” to previously connected agents.

A word on client support

Full disclosure: tools are supported by every MCP client, while resources and prompts are handled well by Claude (Desktop and Code) but ignored by several others for now. Design your servers so the tools are self-sufficient, and treat resources and prompts as progressive enhancement — the ecosystem is catching up quickly.

Where to find everything

  • Units: Source/MARS.MCP.* on the develop branch
  • Demo: Demos/MCPServer — tools, resources, prompts, OAuth, logging, all in one runnable console server
  • Docs: the MCP Servers page
  • Agent Skill: the mars-mcp-server skill now covers resources and prompts too — install via /plugin install mars-curiosity@mars in Claude Code, or with npx skills add andrea-magni/MARS in any SKILL.md-compatible tool

Next on the roadmap: server-initiated SSE streams (which unlock resource subscriptions and change notifications), sessions, and JWKS validation for external identity providers. If you build something with this — a schema resource for your ERP, a guided workflow over your invoicing tools — tell me about it (here, on Delphi Praxis forum or via email).

Sincerely,

Andrea

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.