Summary
The Business Central MCP Server does not pass string tool arguments to the API page unchanged. The
client sends a valid JSON-RPC request, but the value that reaches the record is different, or the
call fails. Reproduced with the standard customers API (API v2.0, page 30009) and the
Create_Customer_PAG30009 tool:
displayName sent |
Result |
O'Brien Ltd |
customer created as O''Brien Ltd ΓÇö the apostrophe is doubled |
"Acme" Corp |
call fails: Invalid JSON. A comma character ',' was expected in scope 'Object'. |
Acme\Corp |
call fails: Invalid JSON. An unrecognized escape sequence '\C' was found in a JSON string value. |
Acme\nCorp (backslash + n) |
customer created with a line break in the name |
Caf\u00e9 Corp (backslash + u00e9) |
customer created as Café Corp |
The same request through the plain OData API v2.0, without the MCP Server, creates O'Brien Ltd
correctly. So the data is changed in the MCP Server, not in the API page.
Because the values are not encoded, a " in one argument can also add properties to the request:
displayName = Inj", "phoneNumber": "123 creates a customer named Inj with phone number 123
(see Security impact).
O'Brien, L'Oréal and D'Angelo are ordinary customer names, and the apostrophe is common in
French, Italian, Ukrainian and other languages. Quotes and backslashes appear in names, addresses
and descriptions too. An MCP client cannot write such values. The apostrophe, \n and \u cases
fail silently: the tool reports success and the record holds a different value.
Environment
- Business Central on-premises.
initialize reports
{'name': 'Microsoft Dynamics 365 Business Central', 'version': '29.0.55365.0', 'description': 'Microsoft Dynamics 365 Business Central MCP Server'}.
The behaviour was the same on the version we ran before upgrading to 29.
- MCP Server configuration
CUSTOMERS: API Tools activated, Unblock Edit Tools on,
APIV2 - Customers (page 30009, API v2.0) added with Allow Read, Create, Modify and Delete.
- Transport: Streamable HTTP, Basic authentication, headers
Company and ConfigurationName.
- Client: a plain JSON-RPC client (Python
json.dumps + urllib), so no MCP host is involved. The same results occur from Claude Code and VS Code.
- The same happens with custom API pages, for both
Modify_* (PATCH) and bound actions with a
Text parameter.
Steps to reproduce
-
initialize, then notifications/initialized.
-
tools/call of Create_Customer_PAG30009. This is the exact request body produced by
json.dumps, so it is valid JSON and the server receives displayName = O'Brien Ltd:
{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "Create_Customer_PAG30009",
"arguments": {"number": "MCPTEST1", "displayName": "O'Brien Ltd", "select": "number,displayName"}}}
-
Read the customer back with List_Customers_PAG30009 (filter: number eq 'MCPTEST1').
Actual results
Every value was sent JSON-encoded by json.dumps, so the server received exactly the text in the
"sent" column. The stored value was read back with List_Customers_PAG30009.
| Customer |
Path |
displayName sent |
Outcome |
displayName stored |
| MCPTEST1 |
MCP Create_Customer_PAG30009 |
O'Brien Ltd |
success |
O''Brien Ltd |
| MCPTEST2 |
MCP Create_Customer_PAG30009 |
"Acme" Corp |
Invalid JSON. A comma character ',' was expected in scope 'Object'. Every two elements in an array and properties of an object must be separated by commas. |
not created |
| MCPTEST3 |
MCP Create_Customer_PAG30009 |
Acme\Corp |
Invalid JSON. An unrecognized escape sequence '\C' was found in a JSON string value. |
not created |
| MCPTEST4 |
MCP Create_Customer_PAG30009 |
Acme\nCorp (backslash + n) |
success |
Acme + line break + Corp |
| MCPTEST5 |
MCP Create_Customer_PAG30009 |
Caf\u00e9 Corp |
success |
Café Corp |
| MCPTEST6 |
OData API v2.0 POST …/companies({id})/customers, no MCP |
O'Brien Ltd |
success |
O'Brien Ltd |
| MCPTEST7 |
MCP Create_Customer_PAG30009 |
\"Acme\" Corp (pre-escaped by the client) |
success |
"Acme" Corp |
Raw tool result for MCPTEST2:
{"result": {"content": [{"type": "text", "text": "{\r\n \"error\": {\r\n \"code\": \"BadRequest\",\r\n \"message\": \"Invalid JSON. A comma character \\u0027,\\u0027 was expected in scope \\u0027Object\\u0027. Every two elements in an array and properties of an object must be separated by commas.\"\r\n }\r\n}"}], "isError": true}}
Security impact: JSON injection through a tool argument
Because the value is inserted into the request body without encoding, a " in one argument can
close the string and add further properties to the OData request. With the same tool:
| Customer |
Path |
displayName sent |
Outcome |
Stored |
| MCPTEST8 |
MCP Create_Customer_PAG30009 |
Inj", "phoneNumber": "123 |
success |
displayName = Inj, phoneNumber = 123 |
A single string argument set a second field that the caller did not pass as an argument. In an
agent scenario the argument value often comes from untrusted content (a document, an e-mail, a web
page the agent processed), so this is a prompt-injection-to-data-injection path: text controls which
fields of the record are written. This is CWE-116 (Improper Encoding or Escaping of Output) leading
to CWE-74 (Injection).
Expected behaviour
Tool arguments are data and must reach the API page unchanged:
- MCP / JSON-RPC.
tools/call carries arguments as a JSON object
(MCP specification, Tools,
JSON-RPC 2.0). After parsing the request, the server
holds the decoded string values, for example O'Brien Ltd and "Acme" Corp. It must not
interpret them again.
- Building the OData request body. The body must be produced by a JSON serializer, which escapes
", \ and control characters as required by
RFC 8259, section 7, and not by inserting the
values into a JSON template. A string property in an OData request body is a plain JSON string
(OData JSON Format v4.01).
- No OData literal escaping in the body. Doubling
' is the escaping rule for string literals
inside the URL, i.e. $filter expressions and key predicates
(OData URL Conventions v4.01).
It must be applied only there, never to values sent in a request body.
- Every write path. The same applies to
Create_* (POST), Modify_* (PATCH) and bound actions
(Text parameters in the action's request body).
Concretely, the customers above must be created as O'Brien Ltd, "Acme" Corp, Acme\Corp,
Acme\nCorp (backslash and n) and Caf\u00e9 Corp (the six characters as sent), exactly as the
OData API does without the MCP Server (MCPTEST6), and MCPTEST8 must be created with
displayName = Inj", "phoneNumber": "123 and no phone number.
Workaround
" and \: the client can JSON-escape the value itself (\", \\) before sending it
(MCPTEST7). No MCP client does this on its own, and it will corrupt data once the server is fixed.
': none. Through the MCP Server the apostrophe is always stored doubled.
Internal work item: AB#653294
Summary
The Business Central MCP Server does not pass string tool arguments to the API page unchanged. The
client sends a valid JSON-RPC request, but the value that reaches the record is different, or the
call fails. Reproduced with the standard
customersAPI (API v2.0, page 30009) and theCreate_Customer_PAG30009tool:displayNamesentO'Brien LtdO''Brien Ltd— the apostrophe is doubled"Acme" CorpInvalid JSON. A comma character ',' was expected in scope 'Object'.Acme\CorpInvalid JSON. An unrecognized escape sequence '\C' was found in a JSON string value.Acme\nCorp(backslash +n)Caf\u00e9 Corp(backslash +u00e9)Café CorpThe same request through the plain OData API v2.0, without the MCP Server, creates
O'Brien Ltdcorrectly. So the data is changed in the MCP Server, not in the API page.
Because the values are not encoded, a
"in one argument can also add properties to the request:displayName=Inj", "phoneNumber": "123creates a customer namedInjwith phone number123(see Security impact).
O'Brien,L'OréalandD'Angeloare ordinary customer names, and the apostrophe is common inFrench, Italian, Ukrainian and other languages. Quotes and backslashes appear in names, addresses
and descriptions too. An MCP client cannot write such values. The apostrophe,
\nand\ucasesfail silently: the tool reports success and the record holds a different value.
Environment
initializereports{'name': 'Microsoft Dynamics 365 Business Central', 'version': '29.0.55365.0', 'description': 'Microsoft Dynamics 365 Business Central MCP Server'}.The behaviour was the same on the version we ran before upgrading to 29.
CUSTOMERS: API Tools activated, Unblock Edit Tools on,APIV2 - Customers(page 30009, API v2.0) added with Allow Read, Create, Modify and Delete.CompanyandConfigurationName.json.dumps+urllib), so no MCP host is involved. The same results occur from Claude Code and VS Code.Modify_*(PATCH) and bound actions with aTextparameter.Steps to reproduce
initialize, thennotifications/initialized.tools/callofCreate_Customer_PAG30009. This is the exact request body produced byjson.dumps, so it is valid JSON and the server receivesdisplayName=O'Brien Ltd:{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "Create_Customer_PAG30009", "arguments": {"number": "MCPTEST1", "displayName": "O'Brien Ltd", "select": "number,displayName"}}}Read the customer back with
List_Customers_PAG30009(filter:number eq 'MCPTEST1').Actual results
Every value was sent JSON-encoded by
json.dumps, so the server received exactly the text in the"sent" column. The stored value was read back with
List_Customers_PAG30009.displayNamesentdisplayNamestoredCreate_Customer_PAG30009O'Brien LtdO''Brien LtdCreate_Customer_PAG30009"Acme" CorpInvalid JSON. A comma character ',' was expected in scope 'Object'. Every two elements in an array and properties of an object must be separated by commas.Create_Customer_PAG30009Acme\CorpInvalid JSON. An unrecognized escape sequence '\C' was found in a JSON string value.Create_Customer_PAG30009Acme\nCorp(backslash + n)Acme+ line break +CorpCreate_Customer_PAG30009Caf\u00e9 CorpCafé CorpPOST …/companies({id})/customers, no MCPO'Brien LtdO'Brien LtdCreate_Customer_PAG30009\"Acme\" Corp(pre-escaped by the client)"Acme" CorpRaw tool result for MCPTEST2:
{"result": {"content": [{"type": "text", "text": "{\r\n \"error\": {\r\n \"code\": \"BadRequest\",\r\n \"message\": \"Invalid JSON. A comma character \\u0027,\\u0027 was expected in scope \\u0027Object\\u0027. Every two elements in an array and properties of an object must be separated by commas.\"\r\n }\r\n}"}], "isError": true}}Security impact: JSON injection through a tool argument
Because the value is inserted into the request body without encoding, a
"in one argument canclose the string and add further properties to the OData request. With the same tool:
displayNamesentCreate_Customer_PAG30009Inj", "phoneNumber": "123displayName=Inj,phoneNumber=123A single string argument set a second field that the caller did not pass as an argument. In an
agent scenario the argument value often comes from untrusted content (a document, an e-mail, a web
page the agent processed), so this is a prompt-injection-to-data-injection path: text controls which
fields of the record are written. This is CWE-116 (Improper Encoding or Escaping of Output) leading
to CWE-74 (Injection).
Expected behaviour
Tool arguments are data and must reach the API page unchanged:
tools/callcarriesargumentsas a JSON object(MCP specification, Tools,
JSON-RPC 2.0). After parsing the request, the server
holds the decoded string values, for example
O'Brien Ltdand"Acme" Corp. It must notinterpret them again.
",\and control characters as required byRFC 8259, section 7, and not by inserting the
values into a JSON template. A string property in an OData request body is a plain JSON string
(OData JSON Format v4.01).
'is the escaping rule for string literalsinside the URL, i.e.
$filterexpressions and key predicates(OData URL Conventions v4.01).
It must be applied only there, never to values sent in a request body.
Create_*(POST),Modify_*(PATCH) and bound actions(
Textparameters in the action's request body).Concretely, the customers above must be created as
O'Brien Ltd,"Acme" Corp,Acme\Corp,Acme\nCorp(backslash andn) andCaf\u00e9 Corp(the six characters as sent), exactly as theOData API does without the MCP Server (MCPTEST6), and MCPTEST8 must be created with
displayName=Inj", "phoneNumber": "123and no phone number.Workaround
"and\: the client can JSON-escape the value itself (\",\\) before sending it(MCPTEST7). No MCP client does this on its own, and it will corrupt data once the server is fixed.
': none. Through the MCP Server the apostrophe is always stored doubled.Internal work item: AB#653294