Saltar al contenido principal
Versión: Siguiente

MCP server

History
ReleaseChanges
21 R6Added

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:

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-Version header contains 2026-07-28,
  • the Mcp-Method header contains the JSON-RPC method, and the Mcp-Name header contains the tool name for tools/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 for server/discover and ping.

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:

MethodToken requiredDescription
server/discoverNoReturns the capabilities of the server (see below)
pingNoReturns an empty result
tools/listYesReturns the tool catalog of the session
tools/callYesCalls 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:

PropertyDescription
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)
instructionsmcp.instructions (empty by default)
cacheScope"public"
ttlMs3600000 (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:

CheckHTTP statusJSON-RPC error
MCP server disabled404, no body
Plain HTTP not allowed (see Secure connections)404, no body
Method other than POST405, no body (Allow: POST)
Origin header present (requests from web browsers)403, no body
Body is not valid JSON400-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 executed202, 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 revision400-32020
Unsupported protocol version (data.supported lists the supported versions)400-32022
Unknown method404-32601

Then, for methods that require a token:

CaseHTTP status
No token, or unknown, invalid, expired or revoked token401 (with a WWW-Authenticate challenge in OAuth mode)
No 4D license available for a new MCP session503 with a Retry-After header and the "No 4D license available" JSON-RPC error
Quota exceeded429 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.trustedProxies that sends X-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.publicURL if it is set (recommended when the server is reachable under several names),
  • otherwise the scheme and the Host header of the request. The X-Forwarded-Proto and X-Forwarded-Host headers are only used when the request comes from a proxy listed in mcp.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 TokenOAuth 2.1 (optional)
Who authenticates the userYour application, its own way, before generating the token4D, through a login page that calls your ds.mcpGrant() function
Token4dfgt_…JWT signed by 4D
PrivilegesGiven when the token is generatedReturned by ds.mcpGrant()
LifetimeExpiry date chosen at generation (366 days at most)15 minutes, renewed with a refresh token
Revocationds.revokeFineGrainedToken()ds.revokeOAuthGrant()
Typical useServices, the Claude Desktop bundle, applications with their own loginInteractive 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.

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.type is "mcp". MCP sessions are shown with this type in the administration window of 4D Server, and in the results of the Process activity and License usage commands.
  • 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 fill Session.storage from 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):

  1. 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.
  2. The client registers and opens the authorization URL in the browser.
  3. 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.
  4. 4D redirects to the client with a one-time code, which the client exchanges for an access token (JWT) and a refresh token.
  5. The client sends the access token with every /mcp request. 4D checks it and applies the privileges returned by ds.mcpGrant().

4D serves the following endpoints, under the public base URL:

EndpointDescription
/.well-known/oauth-protected-resourceProtected resource metadata (RFC 9728)
/.well-known/oauth-authorization-serverAuthorization server metadata (RFC 8414). The issuer is the /mcp URL
/mcp/oauth/authorizeLogin page (GET) and login (POST)
/mcp/oauth/tokenCode exchange and token refresh
/mcp/oauth/registerDynamic 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 (sub claim) and the client, and their user name is the sub claim.

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.

atención

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.

nota

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_id of 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.oauthDynamicRegistration is 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://localhost URLs 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,
  • restrictedByDefault applies 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 optional args array (a function that declares both $args and variadic parameters is not published),
    • a function of an Entity class gets a required key property (string or integer): the primary key of the entity on which the function is called. A function of an Entity class that declares a $key parameter is not published.
  • outputSchema, derived from the return type:
Return typeoutputSchema
cs.<DataClass>EntitySchema of the entity, with the attributes the session can read
cs.<DataClass>Selection{entities, total, truncated}
User classProperties declared with the property keyword
Collection{result: array}
Object, Variant, scalar types, no returnNo output schema
  • annotations: the hints of the behavior tags. openWorldHint is false unless 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 isError result "Invalid parameters", and the function does not run.
  • Arguments are converted: a YYYY-MM-DD string to a Date, a number of seconds to a Time, a base64 string to a 4D.Blob or 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 parameters returns the number of arguments provided), and an omitted parameter followed by a provided one gets the default value of its type. The args items are passed after the declared parameters.
  • For an Entity class function, This is the entity of key, 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 valueResult
Text, number, Boolean, Date, TimeText content (dates as YYYY-MM-DD, times in seconds)
ObjectstructuredContent with the object, and its text rendering
CollectionstructuredContent with {result: [...]}
EntityThe 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, PictureA 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.

caution

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:

  1. The function calls MCP.requireInput() with the fields to display, and returns. 4D answers the call with resultType: "input_required", a form (elicitation/create), and a requestState signed by 4D.
  2. The client displays the form to the user, then calls the tool again with the same arguments, the answers (inputResponses), and the requestState.
  3. 4D checks the requestState and runs the function again from the top. MCP.hasInput() now returns True and MCP.input contains 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 elicitation capability), MCP.requireInput() asks nothing and the call returns the isError result "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:

ToolArgumentsResultPermission
describeModelNoneThe dataclasses the session can access, their readable or writable attributes, and their relations to these dataclassesAny data permission
getEntitydataClass, key, attributesThe entityread
queryEntitiesdataClass, filter, params, attributes, orderBy, limit, skip{entities, total, skip, nextSkip}read
aggregateEntitiesdataClass, attribute, filter, paramscount, and with attribute: countNotNull, sum, average, min, maxread
addEntitydataClass, dataThe created entitycreate
updateEntitydataClass, key, data, stampThe updated entityupdate
dropEntitydataClass, key, stampDrops the entity after a confirmation of the userdrop
getBinaryAttributedataClass, key, attributeA picture, or a download URLread
setBinaryAttributedataClass, key, attribute, data, mimeTypeStores the value, or returns an upload URLupdate
  • key is the primary key of the entity, as a string or an integer.
  • attributes is 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.
  • limit is the maximum number of entities to return: 20 by default, between 1 and mcp.maxResults. skip is the number of entities to skip (0 by default): to read the next page, pass the nextSkip value of the previous page. nextSkip is null on the last page.
  • orderBy contains attribute paths followed by asc or desc (for example "lastName asc, salary desc"). Entities are sorted by orderBy, then by primary key, so that pages neither skip nor repeat entities.
  • In aggregateEntities, average is sum/countNotNull. sum and average are null for non-numeric attributes.
  • In data, attributes are given by name. Dates are YYYY-MM-DD strings 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 __DATACLASS must match the related dataclass, and the related entity must be readable). Binary attributes (use setBinaryAttribute), 1-to-N relations, read-only attributes, __KEY and __STAMP are refused.
  • addEntity always creates a new entity.
  • In updateEntity and dropEntity, pass in stamp the __STAMP value you read: the operation is refused if the entity was modified since.
  • dropEntity always asks the user to confirm the deletion (a confirm boolean 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 written YYYY-MM-DD.
  • Each params item 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 by is refused in filter (use orderBy).
  • 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 with attributes. 1-to-N relations and binary attributes are not returned. Dates are YYYY-MM-DD strings and times are numbers of seconds.

The tools return their result in structuredContent, or an isError result with one of the following messages:

MessageCase
Invalid parameters: ...Missing, unknown, or invalid argument
Formulas are not allowed in filterfilter contains a formula
Entity not foundUnknown 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 readThe stamp does not match (the current stamp is returned)
Entity is locked, retry laterThe entity is locked by another process

Binary attributes​

Binary contents (pictures and blobs) do not go through the model's context:

  • getBinaryAttribute returns a PNG, JPEG, GIF, or WebP picture of 1 MB or less as an image content. For any other value, it returns a download URL {url, method, expiresAt}: a GET request on this URL returns the value with its MIME type.
  • setBinaryAttribute with base64 data of 64 KB or less (once decoded) stores the value directly (mimeType is required for a picture). Without data, it returns an upload URL: a PUT request on this URL with the raw value as body and its Content-Type stores the value. Larger inline data gets 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 initialize and ping itself, and relays the other requests to the 4D /mcp URL,
  • 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 dropEntity confirmation, in a browser page on 127.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:

FieldDescription
4D MCP server URLThe /mcp URL of the 4D web server, for example https://myserver.local/mcp
Fine-Grained TokenA 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 certificateA PEM certificate to trust, for a server whose certificate is self-signed or issued by a private authority
Allow insecure connectionFor 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.

See also​