# Dmvcframework Jsonrpc

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

- Skill: `danieleteti/dmvcframework-jsonrpc` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add danieleteti/dmvcframework-jsonrpc`
- Raw SKILL.md: https://api.skillmd.com/api/skills/danieleteti/dmvcframework-jsonrpc/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: danieleteti (https://skillmd.com/u/danieleteti)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/danieleteti/dmvcframework-jsonrpc

---


# 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

| File | Read it when the task involves |
|------|-------------------------------|
| `reference/client.md` | **Calling** 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](https://github.com/danieleteti/delphimvcframework/tree/master/sources) ·
[samples](https://github.com/danieleteti/delphimvcframework/tree/master/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`):

```delphi
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`:

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

  // Middleware
  // Middleware - 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`

```delphi
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:

```delphi
Result := (RTTIMethod.Visibility = mvPublic) and (RTTIMethod.MethodKind in [mkProcedure, mkFunction]);
Result := Result and not IsReservedMethodName(RTTIMethod.Name);
```

| Rule | Detail |
|------|--------|
| Visibility | **`public` only.** `published` is `mvPublished`, not `mvPublic` — a `published` method is **not** callable. `private`/`protected`/`strict private` are not callable either. |
| Kind | `function` or `procedure`. Constructors, destructors and properties are not callable. |
| Reserved names | `OnBeforeRoutingHook`, `OnBeforeCallHook`, `OnAfterCallHook` can never be invoked remotely. |
| Name matching | Case-insensitive (`SameText`). The samples send `'subtract'` for `function Subtract`. |
| Overloads | The lookup returns the **first** match by name. Do not overload an invokable method. |
| `function` vs `procedure` | Not 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.

```delphi
function Subtract(Value1, Value2: Integer): Integer;
```
```json
{"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 type | JSON |
|-------------|------|
| `string` | string (a non-string raises `Invalid params`) |
| `Integer`, `Int64` | number |
| `Double`, `Extended`, `Single` | number (an integer literal is accepted) |
| `Boolean` | `true`/`false` |
| `TDate`, `TTime`, `TDateTime` | ISO-8601 string |
| enum | its name as a string (or a bool for `Boolean`-kind) |
| set | comma-separated string of element names |
| `TJDOJsonObject` / `TJDOJsonArray` | object / array — passed as a **clone** |
| `TObjectList<T>` and other duck-typed lists | array of objects |
| any other `TObject` descendant | object, deserialized into a newly created instance |
| `record` | object |
| 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 `FreeMem`s 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.

```delphi
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**.

```delphi
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:

```delphi
// 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

| | Request | Notification |
|--|---------|--------------|
| JSON | has an `id` member | **no** `id` member |
| Delphi method | must be a **`function`** | must be a **`procedure`** |
| HTTP response | 200 + `{"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

```delphi
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 is caught and returned as an error with code `0` and the
exception message (plus the class name in `data`), unless the endpoint has an exception handler (below).

### Exception classes (`MVCFramework.JSONRPC.pas`)

| Class | Code it produces |
|-------|------------------|
| `EMVCJSONRPCError` | whatever you pass |
| `EMVCJSONRPCServerError` | whatever 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

```delphi
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:

```delphi
TMVCJSONRPCExceptionHandlerProc = reference to procedure(E: Exception;   // MVCFramework.pas
  WebContext: TWebContext;
  var ErrorInfo: TMVCJSONRPCExceptionErrorInfo;    // record: Code: Integer; Msg: string; Data: TValue
  var ExceptionHandled: Boolean);
```

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

---

## 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:

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

| Hook | When | Notes |
|------|------|-------|
| `OnBeforeRoutingHook` | after the payload is parsed, **before** the method is resolved | receives the raw request JSON; the method name is re-read from it afterwards, so the hook can remap `method` |
| `OnBeforeCallHook` | after parameters are bound, immediately before the invoke | receives the request JSON; raise `EMVCJSONRPCError` here to reject the call |
| `OnAfterCallHook` | after the call, **including on error** | receives 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 middleware applies unchanged — add
`TMVCJWTAuthenticationMiddleware` or `TMVCBasicAuthenticationMiddleware` in `ConfigureEngine` exactly as
documented in `dmvcframework`, and use the `dmvcframework-security` skill for hardening.

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:

| Route | What |
|-------|------|
| `GET /jsonrpc/describe` | `text/plain` listing of every invokable method with its Delphi declaration, its `[MVCDoc]` text, and whether it is POST-only or POST+GET |
| `GET /jsonrpc/proxy` | generated 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**

```delphi
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**

```delphi
uses
  Controllers.HomeU, JSONRPCServiceU, MVCFramework.JSONRPC, MVCFramework.Middleware.CORS;

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

  // Middleware
  AEngine.AddMiddleware(TMVCCORSMiddleware.Create);
  // Middleware - END

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

**Client**

```delphi
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

| Mistake | Reality |
|---------|---------|
| `published` methods on the RPC class | Only **`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 methods | There is no per-method routing. One URL, dispatch by `method` name |
| `[JsonRpcMethod]`, `RegisterMethod`, `AddRpcHandler` | None 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 error | Errors 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 method | The framework frees every object parameter after the call — double free |
| Freeing the object you **return** | The 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]` child | It gets freed. Return a clone |
| Freeing an object after `Params.Add`/`AddByName` on the client | `TJSONRPCRequestParams` owns and frees it |
| `lResp.Error.Message` | The property is **`ErrMessage`** |
| `var`/`out` parameters | Raise `EMVCJSONRPCInvalidParams`. Only `const` and by-value |
| Calling with GET without `[MVCJSONRPCAllowGET]` | `-32600 Invalid Request`, *"Method callable with POST only"* |
| Renaming a formal parameter | Named params match the Delphi parameter name — it is part of the contract |
| `Context` inside a `PublishObject` class | It is a plain `TObject`. Get `TWebContext` from a hook, or descend from `TMVCJSONRPCController` |
| `[MVCRequiresAuthentication]` on the RPC class | Nothing reads it for `TMVCJSONRPCPublisher`. Auth comes from the middleware's `IMVCAuthenticationHandler` |
| Overloading an invokable method | Lookup is by name and returns the first match |

---

## 13. Key units

| Unit | Purpose |
|------|---------|
| `MVCFramework.JSONRPC` | `TMVCJSONRPCController`, `TMVCJSONRPCPublisher`, `[MVCJSONRPCAllowGET]`, `[MVCJSONRPCOptional]`, `[MVCJSONRPCRestParams]`, `TJSONRPCRequest`, `IJSONRPCRequest/Notification/Response`, `TJSONRPCRequestParams`, `TJSONRPCParamDataType`, all `EMVCJSONRPC*` classes, the `JSONRPC_*` constants |
| `MVCFramework.JSONRPC.Client` | `IMVCJSONRPCExecutor`, `IMVCJSONRPCExecutorAsync`, `TMVCJSONRPCExecutor`, `TJSONRPCResponseHandlerProc`, `TJSONRPCErrorHandlerProc` |
| `MVCFramework` | `TMVCEngine.PublishObject`, `TMVCObjectCreatorDelegate`, `TMVCJSONRPCExceptionHandlerProc`, `TMVCJSONRPCExceptionErrorInfo`, `TWebContext`, `[MVCInject]`, `[MVCDoc]`, `[MVCInheritable]` |
| `JsonDataObjects` | `TJDOJsonObject`, `TJDOJsonArray` (aliases of `TJsonObject`/`TJsonArray`) |
| `MVCFramework.Serializer.JsonDataObjects` | `TMVCJsonDataObjectsSerializer` — the serializer used for params and results |
| `MVCFramework.Serializer.Commons` | `TMVCSerializationType` (`stDefault`, `stProperties`, `stFields`), `[MVCNameCase]`, `[MVCNameAs]` |
| `MVCFramework.Server.Factory` | `TMVCServerFactory.CreateIndyDirect / CreateHttpSys / CreateWebBroker` |

---

## 14. Reference samples

| Folder | Shows |
|--------|-------|
| `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.pas` | `IMVCAuthenticationHandler` for this sample set |
| `unittests/general/TestServer/TestServerControllerJSONRPCU.pas` | `TMVCJSONRPCController` 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).

