MCP server
History
| Release | Changes |
|---|---|
| 21 R6 | Added |
The 4D Web Server includes a native Model Context Protocol (MCP) server. AI assistants and agents (Claude Desktop, Claude Code, MCP Inspector, etc.) can connect to it to discover and call the business logic and the data of your application.
The MCP server publishes two kinds of tools:
- the custom tools that you write as ORDA or singleton class functions declared with
exposed MCP Function, - the built-in CRUD tools, which let a client read and write data of the datastore.
You do not need to write the MCP layer: 4D generates the tool catalog, converts the arguments and the results, and applies the same roles and privileges as for REST requests. Clients authenticate with a Fine-Grained Token generated by your application, or through the optional OAuth 2.1 mode.
Enabling the MCP server
The MCP server is disabled by default. It requires:
- the web server of the host application (the MCP server is not available on component web servers),
- scalable sessions. If scalable sessions are disabled, the MCP server does not start, 4D logs a message in the diagnostic log, and
/mcpanswers 404.
You enable the MCP server with the enabled property of the mcp object of the web server. You can set it in the settings parameter of .start(), or at runtime without restarting the web server:
var $webServer : 4D.WebServer:=WEB Server
$webServer.mcp.enabled:=True // /mcp now accepts MCP requests
When you set mcp.enabled to False, the calls being executed finish and get their response, every later request gets a 404 status, and the MCP sessions are closed (their licenses are released).
The mcp object also contains the other MCP server settings (server name, instructions, allowed tools, proxies, etc.). These settings are kept when the web server is restarted.
Example
You start the web server with the MCP server enabled and a description of the application for the clients:
var $settings:={mcp: {enabled: True; serverName: "Sales"; instructions: "Call describeModel first to learn the data model."}}
var $status:=WEB Server.start($settings)
MCP endpoint
The MCP server answers on the /mcp URL of the web server (for example https://myserver.example.com/mcp). It implements the stateless MCP revision 2026-07-28 only, over HTTP or HTTPS.
Every request is a self-describing POST request:
- its body is a single JSON-RPC 2.0 request,
- the
MCP-Protocol-Versionheader contains2026-07-28, - the
Mcp-Methodheader contains the JSON-RPC method, and theMcp-Nameheader contains the tool name fortools/call. These headers must match the body. A value that is not plain ASCII is sent as=?base64?<base64 of the UTF-8 value>?=, - the
Authorization: Bearer <token>header contains the token, except forserver/discoverandping.
For example:
POST /mcp HTTP/1.1
Host: myserver.example.com
Content-Type: application/json
Authorization: Bearer 4dfgt_...
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: queryEntities
{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "queryEntities", "arguments": {"dataClass": "Customer", "filter": "city = :1", "params": ["Paris"]}}}
Responses are application/json. Every result contains a resultType property ("complete", or "input_required" when the user must answer a question). 4D never reads nor sends an Mcp-Session-Id header.
The following methods are supported:
| Method | Token required | Description |
|---|---|---|
server/discover | No | Returns the capabilities of the server (see below) |
ping | No | Returns an empty result |
tools/list | Yes | Returns the tool catalog of the session |
tools/call | Yes | Calls a custom tool or a built-in CRUD tool |
Clients may call tools/list and tools/call directly, without calling server/discover first. The legacy initialize request of previous MCP revisions is not supported.
server/discover
The server/discover result is the same for all callers. It contains:
| Property | Description |
|---|---|
supportedVersions | ["2026-07-28"] |
capabilities | {"tools": {"listChanged": false}} |
_meta["io.modelcontextprotocol/serverInfo"] | name: mcp.serverName, or the application name by default. version: mcp.serverVersion, or by default the version of the built application ("0.0.0" if the application is not built) |
instructions | mcp.instructions (empty by default) |
cacheScope | "public" |
ttlMs | 3600000 (the client can keep the result for one hour) |
Example
You check from 4D that the MCP server answers on the local machine:
var $headers:={}
$headers["MCP-Protocol-Version"]:="2026-07-28"
$headers["Mcp-Method"]:="server/discover"
var $request:=4D.HTTPRequest.new("http://127.0.0.1/mcp"; {method: "POST"; headers: $headers; body: {jsonrpc: "2.0"; id: 1; method: "server/discover"}})
$request.wait()
// $request.response.status is 200
// $request.response.body.result.supportedVersions is ["2026-07-28"]
Request checks and errors
4D checks each request in the following order, before running any code of the application. The first failing check gives the answer:
| Check | HTTP status | JSON-RPC error |
|---|---|---|
| MCP server disabled | 404, no body | |
| Plain HTTP not allowed (see Secure connections) | 404, no body | |
Method other than POST | 405, no body (Allow: POST) | |
Origin header present (requests from web browsers) | 403, no body | |
| Body is not valid JSON | 400 | -32700 |
Body is not a single JSON-RPC request or notification (batch array, response, missing jsonrpc or method) | 400 | -32600 |
JSON-RPC notification (no id): accepted, nothing is executed | 202, no body | |
Missing MCP-Protocol-Version, Mcp-Method or Mcp-Name header, or header that does not match the body. A legacy initialize request gets a message naming the 2026-07-28 revision | 400 | -32020 |
Unsupported protocol version (data.supported lists the supported versions) | 400 | -32022 |
| Unknown method | 404 | -32601 |
Then, for methods that require a token:
| Case | HTTP status |
|---|---|
| No token, or unknown, invalid, expired or revoked token | 401 (with a WWW-Authenticate challenge in OAuth mode) |
| No 4D license available for a new MCP session | 503 with a Retry-After header and the "No 4D license available" JSON-RPC error |
| Quota exceeded | 429 with a Retry-After header |
Errors of the tools themselves are returned as tool results with isError: true (see Calling a custom tool and Built-in CRUD tools). A tool that does not exist or that the session is not allowed to call gets the -32602 "Unknown tool: <name>" JSON-RPC error.
Secure connections
The MCP server uses the TLS configuration of the web server: there is no specific certificate. For a test server with a self-signed certificate, the certificate must be trusted on the client side (for example with the NODE_EXTRA_CA_CERTS environment variable for Node.js clients).
A token is a long-lived secret: 4D never accepts it in clear over the network. Requests to /mcp (and to its binary URLs) over plain HTTP are only served:
- from a loopback address (
127.0.0.1,::1), - or from a reverse proxy listed in
mcp.trustedProxiesthat sendsX-Forwarded-Proto: https(TLS terminated by the proxy).
Any other plain HTTP request gets a 404 status before authentication, and 4D logs a message in the diagnostic log. On a local network without a certificate, you can set mcp.allowInsecureHTTP to True to accept plain HTTP requests (not recommended). This setting never applies to the OAuth endpoints.
Requests with an Origin header are refused (403) to protect the server against DNS rebinding attacks from web browsers.
Proxies and public URL
4D publishes some URLs (OAuth metadata and endpoints, binary URLs). Their base is:
mcp.publicURLif it is set (recommended when the server is reachable under several names),- otherwise the scheme and the
Hostheader of the request. TheX-Forwarded-ProtoandX-Forwarded-Hostheaders are only used when the request comes from a proxy listed inmcp.trustedProxies.
The caller IP address, used to identify MCP sessions and for the OAuth limits, is the address of the connection or, when the connection comes from a trusted proxy, the rightmost address of the X-Forwarded-For header that is not itself a trusted proxy.
Example
Your 4D server is behind a reverse proxy at 10.0.0.5 that terminates TLS for https://sales.example.com:
var $webServer : 4D.WebServer:=WEB Server
var $mcp:={enabled: True; trustedProxies: ["10.0.0.5"]; publicURL: "https://sales.example.com"}
var $status:=$webServer.start({mcp: $mcp})
Authentication
Every request (except server/discover and ping) carries a token in its Authorization: Bearer <token> header. There is no anonymous MCP session. Two kinds of tokens are accepted side by side:
| Fine-Grained Token | OAuth 2.1 (optional) | |
|---|---|---|
| Who authenticates the user | Your application, its own way, before generating the token | 4D, through a login page that calls your ds.mcpGrant() function |
| Token | 4dfgt_… | JWT signed by 4D |
| Privileges | Given when the token is generated | Returned by ds.mcpGrant() |
| Lifetime | Expiry date chosen at generation (366 days at most) | 15 minutes, renewed with a refresh token |
| Revocation | ds.revokeFineGrainedToken() | ds.revokeOAuthGrant() |
| Typical use | Services, the Claude Desktop bundle, applications with their own login | Interactive login of each user in their AI client |
Fine-Grained Tokens
A Fine-Grained Token (FGT) is generated by your application, with the privileges to give to the MCP client, after the user has been authenticated by the application (for example through the authentify() function). You give the token to the user, who enters it in the configuration of the AI client.
ds.generateFineGrainedToken()creates a token. It can only be called from a web or REST session, and only with privileges that the session holds.ds.listFineGrainedTokens()lists the tokens (never the tokens themselves).ds.revokeFineGrainedToken()revokes a token. It stops working at the next request.
The tokens are stored hashed in the fgt.json file of the data folder. The token itself is returned only once, at generation.
When a request comes with an FGT, 4D checks the token at every request, then applies its privileges before running any code of the application. A privilege or role named by the token and removed since from the roles.json file is ignored (4D logs a message). A token left without any declared privilege gets a 401 status.
Example
In the DataStore class, an exposed function lets an authenticated user get a token for their AI client:
// cs.DataStore class
Class extends DataStoreImplementation
exposed Function getMCPToken() : Text
var $expiry:=Add to date(Current date; 0; 3; 0) // valid for 3 months
var $result:=ds.generateFineGrainedToken("salesReader"; "Claude - "+Session.userName; $expiry; {userName: Session.userName})
return $result.token // "4dfgt_..."
MCP sessions
Requests are stateless, but 4D keeps one web session per token and caller IP address (or per user and client in OAuth mode), so that repeated requests reuse the same session:
- When the session is created, it consumes a 4D license. If no license is available, the request gets a 503 status and no session is created.
- Its
Session.info.typeis"mcp". MCP sessions are shown with this type in the administration window of 4D Server, and in the results of theProcess activityandLicense usagecommands. - Its privileges are those of the token. Then 4D calls the
ds.mcpAuthenticated()function of your DataStore class, if defined, once per session: you can use it to fillSession.storagefrom the information stored with the token. - The session closes after the inactivity timeout of web sessions, when its token is revoked or expires, or when the MCP server is disabled. Its license is then released.
OAuth 2.1 mode
In OAuth mode, 4D is the OAuth authorization server: users log in from their AI client through a 4D login page, without any external identity provider. The OAuth mode is enabled when your DataStore class defines the mcpGrant() function. Fine-Grained Tokens remain accepted.
The flow is the standard authorization code flow with PKCE (S256):
- A request without a valid token gets a 401 status with the
WWW-Authenticate: Bearer resource_metadata="<base>/.well-known/oauth-protected-resource"header. The client reads this metadata, then the metadata of the authorization server. - The client registers and opens the authorization URL in the browser.
- 4D displays its login page. When the user submits it, 4D calls
ds.mcpGrant($user; $password; $clientId), which returns the privileges to give, or an empty value to refuse the login. - 4D redirects to the client with a one-time code, which the client exchanges for an access token (JWT) and a refresh token.
- The client sends the access token with every
/mcprequest. 4D checks it and applies the privileges returned byds.mcpGrant().
4D serves the following endpoints, under the public base URL:
| Endpoint | Description |
|---|---|
/.well-known/oauth-protected-resource | Protected resource metadata (RFC 9728) |
/.well-known/oauth-authorization-server | Authorization server metadata (RFC 8414). The issuer is the /mcp URL |
/mcp/oauth/authorize | Login page (GET) and login (POST) |
/mcp/oauth/token | Code exchange and token refresh |
/mcp/oauth/register | Dynamic client registration, when mcp.oauthDynamicRegistration is True |
The metadata URLs are also served with /mcp appended (for example /.well-known/oauth-protected-resource/mcp). These endpoints answer 404 when the OAuth mode is disabled, and over plain HTTP outside loopback and trusted proxies, whatever the mcp.allowInsecureHTTP value.
Tokens and grants
Each login of a user in a client creates a grant, which holds the privileges returned by ds.mcpGrant():
- The access token is a JWT signed by 4D (HS256), valid 15 minutes. It does not contain the privileges, only the id of its grant: 4D checks at every request that the grant still exists.
- The refresh token is valid 30 days after it is issued, and 90 days after the login at the latest. Each refresh issues a new refresh token. If an already used refresh token is presented again, the whole grant is revoked.
- The authorization code is valid 60 seconds and can be used once.
- OAuth sessions are identified by the user (
subclaim) and the client, and their user name is thesubclaim.
You list the grants with ds.listOAuthGrants() and revoke them with ds.revokeOAuthGrant(): the grant's access tokens get a 401 status from the next request, and its refresh token is refused. When you deactivate a user of your application, revoke their grants and Fine-Grained Tokens.
The signing key of the access tokens and the grants are stored in the data folder (fgt.json file). A copy of this file lets its holder use the existing grants: protect the backups of the data folder like the data itself.
The login page checks a user name and a password with ds.mcpGrant(): single sign-on and multi-factor authentication are not supported in this mode. If you need them, authenticate users your own way and generate Fine-Grained Tokens.
Client registration
In OAuth mode, a client is identified either by the HTTPS URL of a Client ID Metadata Document (CIMD) or by an id issued by dynamic client registration (DCR, RFC 7591). This client id is passed to ds.mcpGrant() in $clientId.
- Client ID Metadata Documents: 4D downloads the document over HTTPS only, from a host name (not an IP address) that resolves to public addresses only (no loopback, private, link-local or multicast address), without following redirects, within 5 seconds and 5 KB. The
client_idof the document must be its URL. 4D keeps the document in cache according to its HTTP cache headers, between 5 minutes and 24 hours. - Dynamic client registration: available when
mcp.oauthDynamicRegistrationis True (default). 4D accepts 10 registrations per caller IP address per hour (429 status beyond), and 1000 dynamic clients in total (503 status beyond). A dynamic client that is not used for 30 days is deleted. - Redirect URIs must be HTTPS URLs, or
http://127.0.0.1/http://localhostURLs on any port. Any other redirect URI is refused, at registration or at authorization.
The login page displays the name of the client and the domain of its metadata document, or the host of its redirect URI, so that users can check who asks for access.
To protect the login page against password guessing:
- after 10 failed logins from one caller IP address within 15 minutes, this address gets a 429 status for 15 minutes,
- after 20 failed logins for one user name within 15 minutes, this user name is refused for 15 minutes, with the same message as a wrong password.
Authorization
The MCP server uses the roles.json file of the project, exactly as the REST server. There is nothing specific to MCP to configure:
- a custom tool requires the execute permission on its function (and benefits from its promote permission),
- the built-in CRUD tools require the read, create, update, or drop permission on the dataclass and its attributes,
restrictedByDefaultapplies to the tools as to REST requests.
The tool catalog only lists the tools that the session can call, but every call is checked again. A tool that the session cannot call gets the same "Unknown tool" error as a tool that does not exist (the reason is logged on the server).
Data accessed through the tools goes through ORDA, so restricted entity selections (Function event restrict) apply. You can filter differently for MCP sessions by testing Session.info.type.
Example
MCP clients only see the orders of the user whose ID was stored in Session.storage by ds.mcpAuthenticated():
// cs.Order class
Class extends DataClass
Function event restrict() : cs.OrderSelection
If (Session#Null) && (Session.info.type="mcp")
return This.query("ownerID = :1"; Session.storage.user.ID)
End if
return Null // no filter for other sessions
Custom tools
You publish a class function as an MCP tool by declaring it with the exposed and MCP keywords:
exposed MCP Function <functionName>(<parameters>) : <type>
Functions of the following classes can be published:
- the DataStore class,
- DataClass classes,
- Entity classes,
- shared singleton classes.
Functions declared without exposed or without MCP, and functions of EntitySelection classes, are not published. See exposed MCP Function.
The comment block written above the declaration provides the description of the tool, of its parameters, and its behavior tags. A function without description is published, with a warning in the diagnostic log.
Example
You publish a function of a shared singleton class as a read-only tool:
// cs.OrderManager shared singleton class
shared singleton Class constructor()
// Returns the total amount of the orders of a customer for a year
// $customerID: ID of the customer
// $year: year of the orders
// MCP: @readOnly @idempotent
exposed MCP Function getYearTotal($customerID : Integer; $year : Integer) : Real
return ds.Order.query("customer.ID = :1 and year = :2"; $customerID; $year).sum("amount")
This function is published as the OrderManager-getYearTotal tool.
Tool catalog
The tools/list method returns the custom tools for which the session has the execute permission, and the built-in CRUD tools when the session has a data permission. Tools are sorted by name, in pages of 100 tools (cursor / nextCursor).
The catalog is built from the running code: in interpreted mode, a function you add or modify appears at the next tools/list, without restarting. Each page contains a cacheScope: "private" property and a ttlMs property (0 in interpreted mode, 300000 in compiled mode) that tells the client how long it can keep the catalog.
Each tool is described as follows:
- name:
ClassName-functionName, or the function name for a DataStore class function. The name must match^[A-Za-z0-9_-]{1,64}$and must not be the name of another tool, including a built-in CRUD tool. Otherwise, the function is not published and 4D logs a message. - description: the
MCP:description lines of the comment block, or its description lines. - inputSchema: an object with one property per parameter, named without the
$, with the types converted to JSON Schema as described in MCP tool metadata, and"additionalProperties": false:- every parameter is required, except those documented with
{$name}, - typed variadic parameters (
... : Type) are an optionalargsarray (a function that declares both$argsand variadic parameters is not published), - a function of an Entity class gets a required
keyproperty (string or integer): the primary key of the entity on which the function is called. A function of an Entity class that declares a$keyparameter is not published.
- every parameter is required, except those documented with
- outputSchema, derived from the return type:
| Return type | outputSchema |
|---|---|
cs.<DataClass>Entity | Schema of the entity, with the attributes the session can read |
cs.<DataClass>Selection | {entities, total, truncated} |
| User class | Properties declared with the property keyword |
| Collection | {result: array} |
| Object, Variant, scalar types, no return | No output schema |
- annotations: the hints of the behavior tags.
openWorldHintisfalseunless the function uses@openWorld. Contradictory hints are omitted. Annotations are hints for the client: they do not protect anything (see Asking the user for confirmation).
Calling a custom tool
When a client calls a custom tool (tools/call), 4D checks the execute permission, then the arguments:
- An argument that is missing, unknown, or whose type does not match the schema gives an
isErrorresult "Invalid parameters", and the function does not run. - Arguments are converted: a
YYYY-MM-DDstring to a Date, a number of seconds to a Time, a base64 string to a4D.Blobor a Picture, and a number to a Real for a Variant parameter. - Arguments are passed in the order of the parameters, up to the last one provided: trailing omitted parameters are not passed (
Count parametersreturns the number of arguments provided), and an omitted parameter followed by a provided one gets the default value of its type. Theargsitems are passed after the declared parameters. - For an Entity class function,
Thisis the entity ofkey, loaded with the read permission of the dataclass and its restriction filter. An unknown, filtered, or invisible key gives the "Entity not found" error.
The function runs in the MCP session, with the same permissions as a REST call. The value it returns is converted into a tool result:
| Returned value | Result |
|---|---|
| Text, number, Boolean, Date, Time | Text content (dates as YYYY-MM-DD, times in seconds) |
| Object | structuredContent with the object, and its text rendering |
| Collection | structuredContent with {result: [...]} |
| Entity | The entity, serialized as by the CRUD tools with the attributes the session can read (null if the session cannot read it) |
| Entity selection | {entities, total, truncated}: at most mcp.maxResults entities |
| Blob, Picture | A download URL, valid 10 minutes (see Binary attributes) |
In returned objects and collections, dates are in ISO format, binary values are download URLs, entities are {__KEY, __DATACLASS} and entity selections are {__DATACLASS, __COUNT}.
If the function throws an error, the result is an isError result with the error message. If the client disconnects during the call, the function runs to its end and its result is discarded.
Data read with a promote permission and returned in an object or a collection built by your code is returned as is, whatever the privileges of the session.
Example
You call the OrderManager-getYearTotal tool with:
{"name": "OrderManager-getYearTotal", "arguments": {"customerID": 120, "year": 2026}}
The result contains a text content with the amount, for example "15240.5". If the function throws an error, the client gets it as a tool error:
exposed MCP Function getYearTotal($customerID : Integer; $year : Integer) : Real
If ($year>Year of(Current date))
throw(1; "The year cannot be in the future")
End if
return ds.Order.query("customer.ID = :1 and year = :2"; $customerID; $year).sum("amount")
Asking the user for confirmation
A custom tool can ask the user to confirm an action or to provide values before running it. The MCP command returns the 4D.MCPContext object of the current call:
- The function calls
MCP.requireInput()with the fields to display, and returns. 4D answers the call withresultType: "input_required", a form (elicitation/create), and arequestStatesigned by 4D. - The client displays the form to the user, then calls the tool again with the same arguments, the answers (
inputResponses), and therequestState. - 4D checks the
requestStateand runs the function again from the top.MCP.hasInput()now returns True andMCP.inputcontains the answers.
The requestState is signed by 4D for the token (or OAuth grant), the tool, the converted arguments, the requested fields, and the bind value passed to requireInput(). It is valid 10 minutes, and until the MCP server is disabled. If it is missing, modified, expired, or if the arguments changed, the answers are ignored and the input is asked again. The confirmation is therefore enforced by 4D, whatever the client.
- If the client does not support forms (no
elicitationcapability),MCP.requireInput()asks nothing and the call returns theisErrorresult "This action requires a confirmation your client cannot display": the action never runs. - Do not ask for secrets such as passwords with
MCP.requireInput(). - The bind value is signed, not encrypted: the client can read it.
Example
You want the user to confirm the cancellation of an invoice:
// cs.InvoiceEntity class
Class extends Entity
// Cancels the invoice
exposed MCP Function cancel() : Text
If (Not(MCP.hasInput("confirm")))
MCP.requireInput({message: "Cancel invoice "+String(This.number)+"?"; confirm: {type: "boolean"; title: "Cancel the invoice"}}; This.getStamp())
return
End if
If (MCP.bound#This.getStamp()) // the invoice was modified after the confirmation was asked
MCP.error("The invoice was modified, please try again")
return
End if
If (MCP.input.confirm)
This.status:="cancelled"
This.save()
End if
return This.status
Built-in CRUD tools
The built-in CRUD tools let a client read and write the data of the datastore without custom code. They are listed when the session has at least one data permission (read, create, update, or drop) on a dataclass, and each call requires the corresponding permission:
| Tool | Arguments | Result | Permission |
|---|---|---|---|
describeModel | None | The dataclasses the session can access, their readable or writable attributes, and their relations to these dataclasses | Any data permission |
getEntity | dataClass, key, attributes | The entity | read |
queryEntities | dataClass, filter, params, attributes, orderBy, limit, skip | {entities, total, skip, nextSkip} | read |
aggregateEntities | dataClass, attribute, filter, params | count, and with attribute: countNotNull, sum, average, min, max | read |
addEntity | dataClass, data | The created entity | create |
updateEntity | dataClass, key, data, stamp | The updated entity | update |
dropEntity | dataClass, key, stamp | Drops the entity after a confirmation of the user | drop |
getBinaryAttribute | dataClass, key, attribute | A picture, or a download URL | read |
setBinaryAttribute | dataClass, key, attribute, data, mimeType | Stores the value, or returns an upload URL | update |
keyis the primary key of the entity, as a string or an integer.attributesis a collection of attribute paths to return (dotted paths for related attributes, for example["lastName"; "employer.name"]). By default, all the attributes the session can read are returned.limitis the maximum number of entities to return: 20 by default, between 1 andmcp.maxResults.skipis the number of entities to skip (0 by default): to read the next page, pass thenextSkipvalue of the previous page.nextSkipisnullon the last page.orderBycontains attribute paths followed byascordesc(for example"lastName asc, salary desc"). Entities are sorted byorderBy, then by primary key, so that pages neither skip nor repeat entities.- In
aggregateEntities,averageissum/countNotNull.sumandaveragearenullfor non-numeric attributes. - In
data, attributes are given by name. Dates areYYYY-MM-DDstrings and times are numbers of seconds. An N-to-1 relation is set with the primary key of the related entity:{"employer": {"__KEY": "12"}}(an optional__DATACLASSmust match the related dataclass, and the related entity must be readable). Binary attributes (usesetBinaryAttribute), 1-to-N relations, read-only attributes,__KEYand__STAMPare refused. addEntityalways creates a new entity.- In
updateEntityanddropEntity, pass instampthe__STAMPvalue you read: the operation is refused if the entity was modified since. dropEntityalways asks the user to confirm the deletion (aconfirmboolean field, see Asking the user for confirmation). The entity is dropped only when the confirmation comes from the user's answers, never from the arguments.
You can restrict the available tools with the mcp.crudTools setting (all nine tools by default). A tool that is not in this collection is not listed and gets the "Unknown tool" error. An unknown name in this collection is ignored, and 4D logs a message.
The CRUD tools use the same permissions, restriction filters, and attribute permissions as REST requests. A dataclass or an attribute that the session cannot see gets the same error as one that does not exist, while a visible one with a forbidden operation gets the "Insufficient privileges for <operation> on <dataClass>" error.
Example
You only want to publish the read tools:
var $mcp:={enabled: True; crudTools: ["describeModel"; "getEntity"; "queryEntities"; "aggregateEntities"]}
var $status:=WEB Server.start({mcp: $mcp})
Filters
The filter argument of queryEntities and aggregateEntities uses the ORDA query syntax, with placeholders (:1, :2...) whose values are passed in the params collection. For example: "lastName = :1 AND employer.name = :2".
- Operators are
=,!=,>,>=,<,<=,AND,OR,NOT, and@is the wildcard. Dates are writtenYYYY-MM-DD. - Each
paramsitem is a scalar value,null, a collection of scalar values, or a vector comparison object{vector; metric; threshold}. - Formulas (including
eval()) are refused with the "Formulas are not allowed in filter" error.order byis refused infilter(useorderBy). - Every attribute of a path, including relation attributes, requires the read permission.
Entities in results
Entities returned by the tools contain:
__KEY: the primary key, always as a string,__STAMP: the stamp of the entity,- the attributes the session can read. An N-to-1 relation is returned as
{__KEY, __DATACLASS}, or as an object with the requested attributes when it is projected withattributes. 1-to-N relations and binary attributes are not returned. Dates areYYYY-MM-DDstrings and times are numbers of seconds.
The tools return their result in structuredContent, or an isError result with one of the following messages:
| Message | Case |
|---|---|
| Invalid parameters: ... | Missing, unknown, or invalid argument |
| Formulas are not allowed in filter | filter contains a formula |
| Entity not found | Unknown key, entity filtered by a restriction, or invisible dataclass |
| Insufficient privileges for <operation> on <dataClass> | The operation is not allowed on a visible dataclass |
| Entity was modified since it was read | The stamp does not match (the current stamp is returned) |
| Entity is locked, retry later | The entity is locked by another process |
Binary attributes
Binary contents (pictures and blobs) do not go through the model's context:
getBinaryAttributereturns a PNG, JPEG, GIF, or WebP picture of 1 MB or less as animagecontent. For any other value, it returns a download URL{url, method, expiresAt}: aGETrequest on this URL returns the value with its MIME type.setBinaryAttributewith base64dataof 64 KB or less (once decoded) stores the value directly (mimeTypeis required for a picture). Withoutdata, it returns an upload URL: aPUTrequest on this URL with the raw value as body and itsContent-Typestores the value. Larger inlinedatagets the "Invalid parameters" error.
Binary URLs have the form <public base URL>/mcp/binary/<token>. They are signed by 4D, valid 10 minutes, and do not contain any session id: the URL is the credential, and it remains valid until it expires, even if the token of the session is revoked. An upload URL can be used only once. An invalid, expired, or already used URL gets a 403 status without body. Binary URLs are no longer valid once the MCP server is disabled. The secure connection rules apply to these URLs.
Quotas
MCP requests are web server requests: the web server quotas apply to them, and requests over a quota get a 429 status before any code runs. Each MCP session counts in the session quotas.
Example
You limit each MCP session (and each web session) to 60 requests per minute:
var $webServer : 4D.WebServer:=WEB Server
$webServer.quotas.nbRequestsPerMinPerSession:=60
Claude Desktop bundle
Claude Desktop runs local MCP servers over stdio and does not speak the 2026-07-28 revision. To let it reach a 4D server on the local network without exposing this server on the internet, 4D provides a reference MCP Bundle (.mcpb file). The bundle is a small Node.js adapter run by Claude Desktop:
- it answers
initializeandpingitself, and relays the other requests to the 4D/mcpURL, - it authenticates with a Fine-Grained Token when one is entered, otherwise with the OAuth mode (dynamic client registration and login in the browser),
- it displays the forms of confirmation requests, such as the
dropEntityconfirmation, in a browser page on127.0.0.1, since Claude Desktop does not display them, - when Claude Desktop cancels a request, it cancels the matching request to 4D.
To install it, open the .mcpb file (or drag it in Settings > Extensions of Claude Desktop), then fill in the installation form:
| Field | Description |
|---|---|
| 4D MCP server URL | The /mcp URL of the 4D web server, for example https://myserver.local/mcp |
| Fine-Grained Token | A token generated by the application with ds.generateFineGrainedToken(). Claude Desktop stores it in the keychain of the system. Leave it empty to log in with OAuth |
| CA certificate | A PEM certificate to trust, for a server whose certificate is self-signed or issued by a private authority |
| Allow insecure connection | For tests only. Allows http:// URLs to another machine and disables the certificate check |
The bundle refuses to start, and logs why, when the URL is an http:// URL to another machine and Allow insecure connection is not checked, or when no token is entered and the server does not offer OAuth login with dynamic client registration.
In OAuth mode, the client and its tokens are stored in the .4d-mcp-bundle folder of the user's home folder: delete its subfolder to log out.
Software editors can rebrand the bundle (name, icon, default server URL in its manifest.json file) and repackage it for their customers with the mcpb tool, as described in its README file.