agentleFS
danieletetidanieleteti/delphi-ai-skills/skills/dmvcframework-jsonrpc/SKILL.md

dmvcframework-jsonrpc

Skill23 starsChanged 7 days agoApache-2.0View on GitHub

Use when building or consuming a JSON-RPC 2.0 endpoint with DelphiMVCFramework — publishing a plain Delphi class as remotely callable methods, or calling one from a Delphi client. Triggers on "JSON-RPC", "JSONRPC", "MVCFramework.JSONRPC", "RPC method", "PublishObject", "TMVCJSONRPCController", "TMVCJSONRPCPublisher", "IMVCJSONRPCExecutor", "TMVCJSONRPCExecutor", "TJSONRPCRequest", "IJSONRPCResponse", "MVCJSONRPCAllowGET", "EMVCJSONRPCError", "JSON-RPC notification", "the wizard's JSON-RPC Service preset".

  • Reads credentials

1 other project uses a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

10 rules. About 0 are common, 2 are distinctive, and 8 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈2

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 8

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Never invent an identifier or answer from memory.

    line 24
  2. Distinctive:

    If you still cannot verify it, say so.

    line 29
  3. Very specific:

    Read JSONRPCServiceU.pas and EngineConfigU.pas before writing anything, and follow their conventions.

    line 69
  4. Distinctive:

    Step 2 — if there is no wizard project, stop: Do not scaffold one. Tell the user:

    line 71
  5. Very specific:

    Use this when the RPC methods need the request context: TMVCJSONRPCController descends from TMVCController, so Context is available …

    line 138
  6. Very specific:

    Only const and plain by-value are allowed.

    line 201
  7. Very specific:

    After the call, InvokeMethod walks the parameter array and frees every parameter that is an object (and FreeMems every record).

    line 227
  8. Very specific:

    Do not return a cached or shared object (a singleton, a field, an object owned by a list) — it will be freed under you.

    line 292
  9. Very specific:

    All descend from EMVCJSONRPCErrorResponse, which exposes JSONRPCErrorCode and JSONRPCErrorData.

    line 359
  10. Very specific:

    Not everything reaches the handler: EMVCJSONRPCError and the other EMVCJSONRPCErrorResponse subclasses are sent with the code and message …

    line 426

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

DMVCFramework — JSON-RPC 2.0

A JSON-RPC endpoint is a plain Delphi class published on a URL segment. There are no routing attributes per method, no [MVCPath]: the framework reflects over the class with RTTI and exposes its public methods.

Everything below is copied from sources/MVCFramework.JSONRPC.pas, sources/MVCFramework.JSONRPC.Client.pas, the samples/jsonrpc/ projects and the wizard's generated JSONRPCServiceU.pas. Do not invent names — this API does not follow JSON-RPC library spellings from other ecosystems (there is no RegisterMethod, no [JsonRpcMethod], no AddRpcHandler).

Reference files — read the one you need

FileRead it when the task involves
reference/client.mdCalling a JSON-RPC endpoint from Delphi: IMVCJSONRPCExecutor / TMVCJSONRPCExecutor, ExecuteRequest / ExecuteNotification and their async twins, building an IJSONRPCRequest, reading IJSONRPCResponse, client-side ownership, headers/TLS/tracing

When in doubt about an API — verify it, never guess

Never invent an identifier or answer from memory. If you need a signature this skill does not cover, ask the user for the path to their DelphiMVCFramework checkout and read sources/ (and the matching samples/ project); failing that, read the official repository — sources · samples. If you still cannot verify it, say so. Where a skill and a sample disagree, the sample wins.


Delphi Language Target

Delphi 11 Alexandria or later — inline var declarations and for var loops are fine, and this skill's examples use them. Do not use Delphi 13 Florence-only syntax (NameOf, inline if expressions). For the language and the RTL themselves — version gating, lifetime, strings, generics, threading — load the delphi skill.


REQUIRED REFERENCE — dmvcframework-security. A JSON-RPC endpoint is a single URL that dispatches to arbitrary methods by name from the request body — every public method on the published class is reachable by anyone who can reach the endpoint. Follow that skill for access control, mass assignment, SQL injection, JWT and secrets before shipping. Invoke it whenever you write or review an RPC method — not only when the user says "security".


0. STOP — start from a wizard project

Never create the project from scratch. Never hand-write the .dpr bootstrap.

The IDE wizard ships a JSON-RPC Service preset (DMVC.Expert.Presets.pas, ppJSONRPC) that generates a compiling project with the endpoint already published. Your job is to add methods to it.

Step 1 — detect it. The user is expected to have run the wizard and started the agent from inside the project folder. A JSON-RPC wizard project has:

*.dpr                 Boot + RunServer  (Indy Direct by default)
BootConfigU.pas       dotEnv + LoggerPro
EngineConfigU.pas     ConfigureEngine — AEngine.PublishObject(...) lives HERE
JSONRPCServiceU.pas   TMyJSONRPCService — YOUR RPC METHODS GO HERE
Controllers.HomeU.pas health-check controller
bin/.env              port and settings

Read JSONRPCServiceU.pas and EngineConfigU.pas before writing anything, and follow their conventions.

Step 2 — if there is no wizard project, stop. Do not scaffold one. Tell the user:

This skill works on a project generated by the DMVCFramework IDE wizard, and I do not see one here. Please create it first:

Delphi IDE → File → New → Other → Delphi Projects → DelphiMVCFramework → New DMVCFramework Application

Pick the JSON-RPC Service preset. Accept the defaults — the server backend is Indy Direct. Compile and run it once. Then cd into the project folder, start me there, and tell me which RPC methods to add.

Then wait.

The host may be WebBroker (ISAPI, Apache) — that is fine. The samples/jsonrpc/jsonrpcserver project is WebBroker-hosted and publishes the object from WebModuleCreate. Keep the host, do not migrate it, do not suggest migrating it. Everything above the host is identical on every backend. See dmvcframework, reference/servers.md.


1. Publishing a class — PublishObject

TMVCEngine.PublishObject is the registration method (sources/MVCFramework.pas):

function PublishObject(const AObjectCreatorDelegate: TMVCObjectCreatorDelegate;
  const AURLSegment: string;
  ExceptionHandler: TMVCJSONRPCExceptionHandlerProc = nil): TMVCEngine;

TMVCObjectCreatorDelegate = reference to function: TObject;   // MVCFramework.pas

In a wizard project it goes in ConfigureEngine, in EngineConfigU.pas:

procedure ConfigureEngine(AEngine: TMVCEngine);
begin
  // Controllers
  AEngine.AddController(THomeController);
  // Controllers - END

  AEngine.PublishObject(
    function: TObject
    begin
      Result := TMyJSONRPCService.Create;
    end, '/jsonrpc');
end;
  • The delegate runs once per request — a fresh instance per call, so per-request state in fields is safe.
  • The framework owns and frees the instance it created (PublishObject passes Owns = True to TMVCJSONRPCPublisher.Create). Do not keep a reference and free it yourself.
  • PublishObject returns the engine, so calls chain. You can publish several classes on several segments.
  • Under the hood it is AddController(TMVCJSONRPCPublisher, <delegate>, AURLSegment) — a JSON-RPC endpoint is a controller, so every middleware in the chain (CORS, compression, JWT, ActiveRecord) applies to it.

Alternative: descend from TMVCJSONRPCController

type
  TMyRPC = class(TMVCJSONRPCController)
  public
    function Subtract(Value1, Value2: Int64): Integer;
  end;

AEngine.AddController(TMyRPC, '/jsonrpc');

Use this when the RPC methods need the request context: TMVCJSONRPCController descends from TMVCController, so Context is available directly. Methods declared on that class are invokable; methods inherited from an ancestor are only invokable if marked [MVCInheritable] (MVCInheritableAttribute, declared in MVCFramework).

The wizard uses PublishObject with a plain class. Prefer that unless you need Context.


2. What becomes an RPC method

TMVCJSONRPCController.CanBeRemotelyInvoked is the whole rule:

Result := (RTTIMethod.Visibility = mvPublic) and (RTTIMethod.MethodKind in [mkProcedure, mkFunction]);
Result := Result and not IsReservedMethodName(RTTIMethod.Name);
RuleDetail
Visibilitypublic only. published is mvPublished, not mvPublic — a published method is not callable. private/protected/strict private are not callable either.
Kindfunction or procedure. Constructors, destructors and properties are not callable.
Reserved namesOnBeforeRoutingHook, OnBeforeCallHook, OnAfterCallHook can never be invoked remotely.
Name matchingCase-insensitive (SameText). The samples send 'subtract' for function Subtract.
OverloadsThe lookup returns the first match by name. Do not overload an invokable method.
function vs procedureNot cosmetic — see §5. A function can only be called as a request; a procedure only as a notification.

[MVCDoc('...')] on a method is picked up by the /describe endpoint (§9). It is documentation only.

[MVCJSONRPCAllowGET] on a method also allows the call over HTTP GET; without it, GET is rejected with -32600 Invalid Request / "Method callable with POST only". The check reads the method's own attributes.


3. Parameters

Named or positional — both work

The framework inspects params in the request: a JSON array → positional, a JSON object → named.

Named params are matched against the Delphi formal parameter name, case-insensitively. Rename a parameter and every named-params client breaks.

function Subtract(Value1, Value2: Integer): Integer;
{"jsonrpc":"2.0","method":"subtract","params":[10,3],"id":1}
{"jsonrpc":"2.0","method":"subtract","params":{"Value1":10,"Value2":3},"id":1}

Counts are checked: positional must match exactly; named may not exceed the declared count. A missing named param raises Invalid params unless the parameter carries [MVCJSONRPCOptional], in which case the type's zero value ('', 0, 0.0, False, nil) is passed. Two caveats, both verified in InvokeMethod:

  • [MVCJSONRPCOptional] is honoured only on the named-params path. With a positional array the count must still match exactly.
  • The value passed is the framework's zero value. A Delphi default in the declaration (= False, = 'INFO') is not consulted — RTTI invocation does not apply Delphi defaults.

Parameter modifiers

Only const and plain by-value are allowed. The framework rejects the pfVar, pfOut and pfArray flags — i.e. var, out and open-array (array of …) parameters — with EMVCJSONRPCInvalidParams at call time: "Parameter modifier not supported for formal parameter [...]. Only const and value modifiers are allowed." A TArray<T> parameter is a normal dynamic array and is fine.

Supported parameter types

Verified in JSONDataValueToTValueParamEx (MVCFramework.JSONRPC.pas):

Delphi typeJSON
stringstring (a non-string raises Invalid params)
Integer, Int64number
Double, Extended, Singlenumber (an integer literal is accepted)
Booleantrue/false
TDate, TTime, TDateTimeISO-8601 string
enumits name as a string (or a bool for Boolean-kind)
setcomma-separated string of element names
TJDOJsonObject / TJDOJsonArrayobject / array — passed as a clone
TObjectList<T> and other duck-typed listsarray of objects
any other TObject descendantobject, deserialized into a newly created instance
recordobject
dynamic array / TArray<record>array

Ownership of parameters — the framework frees them

After the call, InvokeMethod walks the parameter array and frees every parameter that is an object (and FreeMems every record). Never free an object parameter inside an RPC method, and never store it past the end of the call — take a copy if you need to keep it.

function TMyJSONRPCService.SavePerson(const Person: TPerson): Integer;
begin
  Result := fRepo.Save(Person);   // correct — do NOT Person.Free
end;

Returning the parameter object as the result is also wrong: the result is freed too (§4) → double free.

Injecting services — [MVCInject]

A parameter marked [MVCInject] is not read from params; it is resolved from the service container and does not count towards the parameter count. Its type must be an interface.

function GetCustomer(const ID: Integer;
  [MVCInject] CustomerService: ICustomerService): TCustomer;

[MVCInject('name')] selects a named registration (MVCInjectAttribute.ServiceName). See dmvcframework, reference/di-and-repository.md.

Collecting extra params — [MVCJSONRPCRestParams]

On the last parameter, which must be TJDOJsonArray: every parameter the client sent that the signature does not declare is collected into that array, each item an object with "value" and (for named params) "name". Written for protocols like MCP that add fields such as _meta. Without it, extra params are an error.


4. Return values and ownership

The result of a function is serialized into the result member by TMVCJsonDataObjectsSerializer.TValueToJSONObjectProperty — the same serializer that renders controller actions. Scalars, strings, TDate/TTime/TDateTime, enums, sets, nullables from MVCFramework.Nullables, records and arrays of records all work. TDataSet becomes a JSON array of rows; TObjectList<T> becomes a JSON array; TJDOJsonObject/TJDOJsonArray are embedded as-is.

The framework frees the object you return. TJSONRPCResponse.Destroy does if FResult.IsObject then FResult.AsObject.Free. So:

// CORRECT — allocate and return; the framework frees it
function TMyJSONRPCService.GetUser(aUserName: string): TPerson;
begin
  Result := TPerson.Create;
  Result.FirstName := aUserName;
end;

function TMyJSONRPCService.GetServerInfo: TJDOJsonObject;
begin
  Result := TJDOJsonObject.Create;
  Result.S['serverName'] := 'DMVCFramework JSON-RPC Server';
end;

There is no ToFree here — ToFree<T> is a TMVCRenderer helper for controller actions, and the published RPC class is a plain TObject that does not have it. Free your own intermediates with try/finally, exactly as TMyObject.GetCustomers does in samples/jsonrpc/jsonrpcserver/MyObjectU.pas.

Do not return a cached or shared object (a singleton, a field, an object owned by a list) — it will be freed under you. Return a clone: the sample does Result := WithJSON.Clone as TJsonObject.

A procedure returns nothing; the response is HTTP 204 with no body (§5).


5. Requests vs notifications — and no batch

RequestNotification
JSONhas an id memberno id member
Delphi methodmust be a functionmust be a procedure
HTTP response200 + {"jsonrpc":"2.0","result":…,"id":…}204 No Content, empty body

Both directions are enforced. Calling a procedure with an id raises "Cannot call a procedure using a JSON-RPC request - use requests for functions and notifications for procedures"; calling a function without an id raises the mirror message. Both come back as -32602 Invalid params.

id may be a string or an integer. An id present but of another type is rejected.

Batch requests are not supported. The payload is parsed as a single JSON object (GetPayloadFromRequest → StrToJSONObject); an array of calls is not dispatched. One call per HTTP request. Do not tell the user otherwise.


6. Errors

Errors are returned with HTTP status 200, in the JSON body, per the JSON-RPC 2.0 spec. The framework comments say so explicitly and set ResponseStatus(200) in every exception branch. Do not write a client that keys off the HTTP status.

Raising an error from an RPC method

uses MVCFramework.JSONRPC;

raise EMVCJSONRPCError.Create(JSONRPC_USER_ERROR + 1, 'Division by zero');
raise EMVCJSONRPCError.Create(JSONRPC_USER_ERROR + 2, 'Not found', lExtraJsonObject);  // "data" member
raise EMVCJSONRPCError.CreateFmt(JSONRPC_USER_ERROR + 3, 'Customer %d is locked', [ID]);

An object passed as Data is freed by the framework (TJSONRPCResponseError.Destroy). Do not free it.

Any other exception that escapes an RPC method (or an OnAfterCall hook) is caught and returned as an error with code 0. In DEBUG the message is Exception.Message. Outside DEBUG it is filtered, as REST does: the message of an EMVCException and the unit's own protocol errors (EMVCJSONRPCException, EMVCJSONRPCInvalidVersion) pass; anything else — a FireDAC error with its SQL, an I/O error with its path — becomes Internal server error. The class name in data (added when a handler leaves ExceptionHandled = False) and a serialization error's DetailedMessage are DEBUG-only.

An EMVCException message still goes out in release: do not put internals in one. To log the detail and choose the message yourself, install the exception handler below.

Exception classes (MVCFramework.JSONRPC.pas)

ClassCode it produces
EMVCJSONRPCErrorwhatever you pass
EMVCJSONRPCServerErrorwhatever you pass — intended for the reserved server range
EMVCJSONRPCParseError-32700
EMVCJSONRPCInvalidRequest-32600
EMVCJSONRPCMethodNotFound-32601
EMVCJSONRPCInvalidParams-32602
EMVCJSONRPCInternalError-32603

All descend from EMVCJSONRPCErrorResponse, which exposes JSONRPCErrorCode and JSONRPCErrorData. Client side: EMVCJSONRPCRemoteException (ErrCode, ErrMessage, Data) and its descendant EMVCJSONRPCProtocolException. EMVCJSONRPCException and EMVCJSONRPCInvalidVersion are plain exceptions.

Reserved codes

JSONRPC_ERR_PARSE_ERROR             = -32700;
JSONRPC_ERR_INVALID_REQUEST         = -32600;
JSONRPC_ERR_METHOD_NOT_FOUND        = -32601;
JSONRPC_ERR_INVALID_PARAMS          = -32602;
JSONRPC_ERR_INTERNAL_ERROR          = -32603;
JSONRPC_ERR_SERVER_ERROR_LOWERBOUND = -32099;
JSONRPC_ERR_SERVER_ERROR_UPPERBOUND = -32000;
JSONRPC_USER_ERROR                  = JSONRPC_ERR_SERVER_ERROR_LOWERBOUND;   // -32099

Application errors go in -32099 .. -32000. Idiom used by the samples and the wizard: JSONRPC_USER_ERROR + n.

Per-endpoint exception handler

The third argument of PublishObject maps arbitrary Delphi exceptions onto JSON-RPC error objects:

TMVCJSONRPCExceptionHandlerProc = reference to procedure(E: Exception;   // MVCFramework.pas
  WebContext: TWebContext;
  var ErrorInfo: TMVCJSONRPCExceptionErrorInfo;    // record: Code: Integer; Msg: string; Data: TValue
  var ExceptionHandled: Boolean);
AEngine.PublishObject(
  function: TObject
  begin
    Result := TMyJSONRPCService.Create;
  end, '/jsonrpc',
  procedure(Exc: Exception; WebContext: TWebContext;
    var ErrorInfo: TMVCJSONRPCExceptionErrorInfo; var ExceptionHandled: Boolean)
  begin
    if Exc is EDivByZero then
    begin
      ErrorInfo.Code := 888;
      ErrorInfo.Msg  := 'Custom Message: ' + Exc.Message;
      ErrorInfo.Data := 'You cannot divide by 0';   // TValue: string or a TObject
      ExceptionHandled := True;
    end
    else
      ExceptionHandled := False;
  end);

An object in ErrorInfo.Data is freed by the framework in both the handled and unhandled paths.

The handler receives the full exception; the ErrorInfo.Msg it starts with is already the filtered one. End it with a catch-all to log the detail and send a message you chose:

    else
    begin
      LogE(Exc.ClassName + ': ' + Exc.Message);   // the detail stays in the log
      ErrorInfo.Code := JSONRPC_USER_ERROR;
      ErrorInfo.Msg  := 'Internal error';
      ExceptionHandled := True;
    end;

Not everything reaches the handler: EMVCJSONRPCError and the other EMVCJSONRPCErrorResponse subclasses are sent with the code and message you chose, and EMVCDeserializationException / EMVCSerializationException are sent as -32600 / -32603 with their own message.


7. Hooks

Declare any of these three on the published class. The names are matched by RTTI and are exact — OnBeforeRoutingHook, OnBeforeCallHook, OnAfterCallHook. The signature is fixed; a wrong one raises EMVCJSONRPCException at call time rather than being ignored:

procedure OnBeforeRoutingHook(const Context: TWebContext; const JSON: TJDOJsonObject);
procedure OnBeforeCallHook(const Context: TWebContext; const JSONRequest: TJDOJsonObject);
procedure OnAfterCallHook(const Context: TWebContext; const JSONResponse: TJDOJsonObject);

Both parameters must be const, the method must be a procedure, and there must be exactly two of them.

HookWhenNotes
OnBeforeRoutingHookafter the payload is parsed, before the method is resolvedreceives the raw request JSON; the method name is re-read from it afterwards, so the hook can remap method
OnBeforeCallHookafter parameters are bound, immediately before the invokereceives the request JSON; raise EMVCJSONRPCError here to reject the call
OnAfterCallHookafter the call, including on errorreceives the response JSON — nil for a notification; always test if Assigned(JSONResponse)

These are the only place a PublishObject-published class sees the TWebContext. The instance is per-request, so stashing Context in a field from OnBeforeCallHook is safe.


8. Authentication

The JSON-RPC endpoint is a controller, so the standard authentication bundles apply unchanged — add AEngine.Use(JWTAuthenticationBundle(...)) (or AEngine.Use(BasicAuthenticationBundle(handler))) in ConfigureEngine exactly as documented in dmvcframework: the check of the bundle, an engine-wide endpoint filter, runs for the published object too. Use the dmvcframework-security skill for hardening. (A 3.4 project may have TMVCJWTAuthenticationMiddleware or TMVCBasicAuthenticationMiddleware: deprecated in 3.5, still working.)

What is different:

  • [MVCRequiresAuthentication] has nothing to attach to. With PublishObject the controller class is the framework's own TMVCJSONRPCPublisher. Auth is decided by your IMVCAuthenticationHandler: OnRequest(AContext, AControllerQualifiedClassName, AActionName, var AAuthenticationRequired) is called with the controller class name and action, not your RPC class or method name. Gate on the URL (AContext.Request.PathInfo) if you need finer granularity, or descend from TMVCJSONRPCController and put the attribute on your own class.
  • Authorization is all-or-nothing at the endpoint. The handler runs before the body is parsed, so it cannot see which RPC method was requested. For per-method authorization, check inside OnBeforeCallHook — you have both Context.LoggedUser and JSONRequest.S['method'] there — and raise EMVCJSONRPCError to reject.
  • The login endpoint of the JWT middleware is a separate URL segment; it is not an RPC method.

samples/jsonrpc/AuthenticationU.pas has a TAuthenticationSample implementing IMVCAuthenticationHandler for this sample set.


9. Introspection endpoints

TMVCJSONRPCController publishes two extra actions relative to the segment:

RouteWhat
GET /jsonrpc/describetext/plain listing of every invokable method with its Delphi declaration, its [MVCDoc] text, and whether it is POST-only or POST+GET
GET /jsonrpc/proxygenerated client code — ?language=delphi (default), ?content-type=…

/proxy requires a proxy generator to have been registered via RegisterJSONRPCProxyGenerator(aLanguage, aClass); no generator ships in sources/, so out of the box this route raises "No Proxy Generators have been registered". /describe always works. Both are public — put them behind auth or a middleware if the method list is sensitive.


10. The client — IMVCJSONRPCExecutor

Calling a JSON-RPC endpoint from Delphi: read reference/client.md. It covers TMVCJSONRPCExecutor.Create, ExecuteRequest / ExecuteNotification and their async twins, building an IJSONRPCRequest with positional or named params, reading the response (Result, ResultAs, Error), error handling, headers/TLS/tracing, and the client-side ownership rules.

Two things to know before you write a single line of client code:

  • TJSONRPCRequestParams owns and frees every object you hand it via Params.Add / AddByName.
  • With the default aRaiseExceptionOnError = True, an error response raises EMVCJSONRPCRemoteException — read .ErrCode / .ErrMessage / .Data from that, not from lResp.Error.

11. Worked example, end to end

JSONRPCServiceU.pas — the published class

unit JSONRPCServiceU;

interface

uses
  MVCFramework, MVCFramework.Commons, MVCFramework.JSONRPC, JsonDataObjects,
  System.Generics.Collections, EntitiesU;

type
  TMyJSONRPCService = class
  public
    [MVCDoc('Divides A by B. Raises a JSON-RPC error if B is zero.')]
    function Divide(const A, B: Double): Double;

    [MVCDoc('Returns a customer by id.')]
    function GetCustomer(const ID: Integer): TCustomer;

    // MaxRows may be omitted — but only by a client sending NAMED params.
    [MVCDoc('Searches customers. Callable with GET too.')]
    [MVCJSONRPCAllowGET]
    function SearchCustomers(const Query: string;
      [MVCJSONRPCOptional] const MaxRows: Integer): TObjectList<TCustomer>;

    // Notification: the client sends no "id"; the server answers 204 with no body.
    [MVCDoc('Logs a message on the server.')]
    procedure LogMessage(const AMessage: string; const ALevel: string);

    procedure OnBeforeCallHook(const Context: TWebContext; const JSONRequest: TJDOJsonObject);
    procedure OnAfterCallHook(const Context: TWebContext; const JSONResponse: TJDOJsonObject);
  end;

implementation

uses
  System.SysUtils, MVCFramework.Logger, MVCFramework.ActiveRecord;

function TMyJSONRPCService.Divide(const A, B: Double): Double;
begin
  if B = 0 then
    raise EMVCJSONRPCError.Create(JSONRPC_USER_ERROR + 1, 'Division by zero');
  Result := A / B;
end;

function TMyJSONRPCService.GetCustomer(const ID: Integer): TCustomer;
begin
  Result := TMVCActiveRecord.GetByPk<TCustomer>(ID);   // framework frees the result
end;

function TMyJSONRPCService.SearchCustomers(const Query: string;
  [MVCJSONRPCOptional] const MaxRows: Integer): TObjectList<TCustomer>;
begin
  Result := TMVCActiveRecord.Where<TCustomer>('name like ?', ['%' + Query + '%']);
end;

procedure TMyJSONRPCService.LogMessage(const AMessage, ALevel: string);
begin
  LogI(Format('[%s] %s', [ALevel, AMessage]));
end;

procedure TMyJSONRPCService.OnBeforeCallHook(const Context: TWebContext;
  const JSONRequest: TJDOJsonObject);
begin
  // Per-method authorization: raise EMVCJSONRPCError here to reject the call.
  LogI('JSON-RPC call: ' + JSONRequest.S['method']);
end;

procedure TMyJSONRPCService.OnAfterCallHook(const Context: TWebContext;
  const JSONResponse: TJDOJsonObject);
begin
  if Assigned(JSONResponse) then    // nil for notifications
    LogI('JSON-RPC response ready for id: ' + JSONResponse.S['id']);
end;

end.

EngineConfigU.pas — registration

uses
  Controllers.HomeU, JSONRPCServiceU, MVCFramework.JSONRPC, MVCFramework.Filters,
  MVCFramework.Filters.ActiveRecord;

procedure ConfigureEngine(AEngine: TMVCEngine);
begin
  // Controllers
  AEngine.AddController(THomeController);
  // Controllers - END

  // HTTPFilter chain (engine-wide). Order matters: first = outermost
  AEngine.Use(CORSHTTPFilter('*'));
  // GetCustomer/SearchCustomers use ActiveRecord: it needs a connection per request
  AEngine.Use(ActiveRecordHTTPFilter('MyConnDef'));

  AEngine.PublishObject(
    function: TObject
    begin
      Result := TMyJSONRPCService.Create;
    end, '/jsonrpc');
end;

Client

uses MVCFramework.JSONRPC, MVCFramework.JSONRPC.Client;

var lExecutor: IMVCJSONRPCExecutor := TMVCJSONRPCExecutor.Create('http://localhost:8080');

// request → function
var lReq := lExecutor.CreateRequest('getcustomer', 1);
lReq.Params.AddByName('ID', 42);
var lCustomer := TCustomer.Create;
try
  lExecutor.ExecuteRequest('/jsonrpc', lReq).ResultAs(lCustomer);
  Writeln(lCustomer.Name);
finally
  lCustomer.Free;
end;

// notification → procedure
var lNot := lExecutor.CreateNotification('logmessage');
lNot.Params.AddByName('AMessage', 'hello');
lNot.Params.AddByName('ALevel', 'INFO');
lExecutor.ExecuteNotification('/jsonrpc', lNot);

Testing an RPC endpoint over real HTTP: dmvcframework-testing.


12. Common mistakes

MistakeReality
published methods on the RPC classOnly public is invokable. published is mvPublished and is silently invisible
AEngine.AddController(TMyJSONRPCService)A plain class is not a controller. Use AEngine.PublishObject(<delegate>, '/jsonrpc')
[MVCPath] / [MVCHTTPMethod] on RPC methodsThere is no per-method routing. One URL, dispatch by method name
[JsonRpcMethod], RegisterMethod, AddRpcHandlerNone exist. Nothing marks a method as RPC — visibility does
A function called as a notification (or a procedure as a request)Rejected with -32602. function = request, procedure = notification
Expecting HTTP 400/404/500 on an errorErrors always come back with HTTP 200 and an error member. 204 means a notification succeeded
Sending a batch (JSON array of calls)Not supported. One call per HTTP request
Freeing an object parameter inside the methodThe framework frees every object parameter after the call — double free
Freeing the object you returnThe framework frees the result too — double free
Result := ToFree(...)ToFree is a TMVCRenderer method; a PublishObject-published class does not have it
Returning a shared/cached object or a [MVCOwned] childIt gets freed. Return a clone
Freeing an object after Params.Add/AddByName on the clientTJSONRPCRequestParams owns and frees it
lResp.Error.MessageThe property is ErrMessage
var/out parametersRaise EMVCJSONRPCInvalidParams. Only const and by-value
Calling with GET without [MVCJSONRPCAllowGET]-32600 Invalid Request, "Method callable with POST only"
Renaming a formal parameterNamed params match the Delphi parameter name — it is part of the contract
Context inside a PublishObject classIt is a plain TObject. Get TWebContext from a hook, or descend from TMVCJSONRPCController
[MVCRequiresAuthentication] on the RPC classNothing reads it for TMVCJSONRPCPublisher. Auth comes from the middleware's IMVCAuthenticationHandler
Overloading an invokable methodLookup is by name and returns the first match

13. Key units

UnitPurpose
MVCFramework.JSONRPCTMVCJSONRPCController, TMVCJSONRPCPublisher, [MVCJSONRPCAllowGET], [MVCJSONRPCOptional], [MVCJSONRPCRestParams], TJSONRPCRequest, IJSONRPCRequest/Notification/Response, TJSONRPCRequestParams, TJSONRPCParamDataType, all EMVCJSONRPC* classes, the JSONRPC_* constants
MVCFramework.JSONRPC.ClientIMVCJSONRPCExecutor, IMVCJSONRPCExecutorAsync, TMVCJSONRPCExecutor, TJSONRPCResponseHandlerProc, TJSONRPCErrorHandlerProc
MVCFrameworkTMVCEngine.PublishObject, TMVCObjectCreatorDelegate, TMVCJSONRPCExceptionHandlerProc, TMVCJSONRPCExceptionErrorInfo, TWebContext, [MVCInject], [MVCDoc], [MVCInheritable]
JsonDataObjectsTJDOJsonObject, TJDOJsonArray (aliases of TJsonObject/TJsonArray)
MVCFramework.Serializer.JsonDataObjectsTMVCJsonDataObjectsSerializer — the serializer used for params and results
MVCFramework.Serializer.CommonsTMVCSerializationType (stDefault, stProperties, stFields), [MVCNameCase], [MVCNameAs]
MVCFramework.Server.FactoryTMVCServerFactory.CreateIndyDirect / CreateHttpSys / CreateWebBroker

14. Reference samples

FolderShows
samples/jsonrpc/jsonrpcserver/PublishObject (three endpoints, one with an exception handler); MyObjectU.pas covers datasets, objects, enums, sets, records, arrays, hooks and error raising
samples/jsonrpc/sync_client/IMVCJSONRPCExecutor, positional and named params, ResultAs, notifications, GET calls, tracing callbacks, HTTP headers
samples/jsonrpc/async_client/IMVCJSONRPCExecutorAsync, ExecuteRequestAsync / ExecuteNotificationAsync
samples/jsonrpc/AuthenticationU.pasIMVCAuthenticationHandler for this sample set
unittests/general/TestServer/TestServerControllerJSONRPCU.pasTMVCJSONRPCController descendants, [MVCInheritable], records and complex types over the wire

Related skills: dmvcframework (core: engine, middleware, entities, DI) · dmvcframework-security (required for any endpoint a stranger can call) · dmvcframework-testing (DUnitX integration tests).

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.