@stubbedev/trimit-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +230 -0
- package/dist/categories/customers.js +187 -0
- package/dist/categories/exported.js +105 -0
- package/dist/categories/index.js +9 -0
- package/dist/categories/inventory.js +63 -0
- package/dist/categories/masterdata.js +246 -0
- package/dist/categories/metadata.js +27 -0
- package/dist/categories/postedsales.js +155 -0
- package/dist/categories/products.js +176 -0
- package/dist/categories/salesdocs.js +395 -0
- package/dist/categories/standard.js +228 -0
- package/dist/common.js +91 -0
- package/dist/index.js +10 -0
- package/dist/registry.js +27 -0
- package/dist/server.js +706 -0
- package/package.json +56 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { ToolRegistry } from "./registry.js";
|
|
5
|
+
import { standardTools, masterdataTools, productsTools, inventoryTools, customersTools, salesdocsTools, postedsalesTools, exportedTools, metadataTools, } from "./categories/index.js";
|
|
6
|
+
import { HOST, STD_API_PATH, TRIMIT_API_PATH, TRIMIT_AUTH, buildHeaders, trimitBase } from "./common.js";
|
|
7
|
+
// Static search lookup for search_trimit_api bootstrap tool
|
|
8
|
+
const TRIMIT_API_SEARCH_MAP = [
|
|
9
|
+
// standard
|
|
10
|
+
{ keywords: ["company", "companies", "company id", "list companies", "tenant company"], endpoint: "/api/v2.0/companies", method: "GET", description: "List BC companies (standard MS API). Gives the companyId GUID required for every TRIMIT call.", category: "standard" },
|
|
11
|
+
{ keywords: ["odata company", "company odata", "odatav4 company"], endpoint: "/ODataV4/Company", method: "GET", description: "List companies via OData v4 endpoint.", category: "standard" },
|
|
12
|
+
{ keywords: ["bc items", "standard items", "items standard"], endpoint: "/api/v2.0/companies({id})/items", method: "GET", description: "Standard BC items list (not TRIMIT-enriched).", category: "standard" },
|
|
13
|
+
{ keywords: ["bc customer", "standard customer", "customer standard", "customer dimensions"], endpoint: "/api/v2.0/companies({id})/customers", method: "GET", description: "Standard BC customers with default dimensions.", category: "standard" },
|
|
14
|
+
{ keywords: ["patch customer", "update customer dimensions", "default dimensions patch"], endpoint: "/api/v2.0/companies({id})/customers({customerId})", method: "PATCH", description: "Patch a customer's default dimensions (standard BC API).", category: "standard" },
|
|
15
|
+
{ keywords: ["metadata", "edmx", "csdl", "schema", "$metadata"], endpoint: "/api/v2.0/$metadata", method: "GET", description: "EDMX schema for the standard MS BC API.", category: "standard" },
|
|
16
|
+
{ keywords: ["entity definitions", "entity definition"], endpoint: "/api/v2.0/entityDefinitions", method: "GET", description: "Entity definitions list.", category: "standard" },
|
|
17
|
+
// masterdata
|
|
18
|
+
{ keywords: ["campaign", "campaigns", "trimit campaign", "promotion"], endpoint: "/companies({id})/campaigns", method: "GET", description: "TRIMIT campaigns with priceGroupParameters and dimensions.", category: "masterdata" },
|
|
19
|
+
{ keywords: ["price group", "customer price group", "price groups"], endpoint: "/companies({id})/customerPriceGroups", method: "GET", description: "Customer price groups.", category: "masterdata" },
|
|
20
|
+
{ keywords: ["price group parameters", "priceGroupParameters", "price params"], endpoint: "/companies({id})/priceGroupParameters", method: "GET", description: "Price group parameter rows.", category: "masterdata" },
|
|
21
|
+
{ keywords: ["vardim combinations", "variant combination", "variant pairs"], endpoint: "/companies({id})/VarDimCombinations", method: "GET", description: "Allowed VarDim combinations.", category: "masterdata" },
|
|
22
|
+
{ keywords: ["vardim", "variant dimension", "vardimtypes"], endpoint: "/companies({id})/vardimtypes", method: "GET", description: "Variant dimension types.", category: "masterdata" },
|
|
23
|
+
{ keywords: ["vardim values", "variant dimension values", "vardimtypevalues"], endpoint: "/companies({id})/vardimtypevalues", method: "GET", description: "Variant dimension values.", category: "masterdata" },
|
|
24
|
+
{ keywords: ["item attribute", "attributes", "bc attribute"], endpoint: "/companies({id})/itemAttributes", method: "GET", description: "Item attributes + values.", category: "masterdata" },
|
|
25
|
+
{ keywords: ["collection", "collections", "season", "season grouping", "delivery period"], endpoint: "/companies({id})/collections", method: "GET", description: "TRIMIT collections with deliveryPeriods.", category: "masterdata" },
|
|
26
|
+
// products
|
|
27
|
+
{ keywords: ["master", "masters", "style", "style master", "master product"], endpoint: "/companies({id})/masters", method: "GET", description: "TRIMIT Masters (style/master products) with full expand.", category: "products" },
|
|
28
|
+
{ keywords: ["item", "items", "sku", "trimit item", "variant sku"], endpoint: "/companies({id})/items", method: "GET", description: "TRIMIT-enriched items / SKUs.", category: "products" },
|
|
29
|
+
{ keywords: ["product", "products", "product feed", "storefront"], endpoint: "/companies({id})/products", method: "GET", description: "Curated TRIMIT product feed (Masters + Items in configured categories).", category: "products" },
|
|
30
|
+
{ keywords: ["category", "categories", "product category"], endpoint: "/companies({id})/categories", method: "GET", description: "TRIMIT categories.", category: "products" },
|
|
31
|
+
// inventory
|
|
32
|
+
{ keywords: ["location", "warehouse", "locations"], endpoint: "/companies({id})/locations", method: "GET", description: "Warehouses / locations master.", category: "inventory" },
|
|
33
|
+
{ keywords: ["inventory", "stock", "availability", "on hand", "future delivers"], endpoint: "/companies({id})/inventories", method: "GET", description: "Inventory / availability per (item, location).", category: "inventory" },
|
|
34
|
+
// customers
|
|
35
|
+
{ keywords: ["customer", "customers", "trimit customer", "customer enriched"], endpoint: "/companies({id})/customers", method: "GET", description: "TRIMIT-enriched customers.", category: "customers" },
|
|
36
|
+
{ keywords: ["contact", "contacts"], endpoint: "/companies({id})/contacts", method: "GET", description: "Contact list.", category: "customers" },
|
|
37
|
+
{ keywords: ["salesperson", "salespersons", "sales rep"], endpoint: "/companies({id})/salespersons", method: "GET", description: "Salespersons list.", category: "customers" },
|
|
38
|
+
{ keywords: ["create customer", "new customer", "post customer"], endpoint: "/companies({id})/customers", method: "POST", description: "Create a TRIMIT customer.", category: "customers" },
|
|
39
|
+
{ keywords: ["patch customer", "update customer", "edit customer", "additional fields"], endpoint: "/companies({id})/customers({customerId})", method: "PATCH", description: "Patch a TRIMIT customer.", category: "customers" },
|
|
40
|
+
// salesdocs
|
|
41
|
+
{ keywords: ["sales document", "salesDocuments", "sales doc", "sales journal"], endpoint: "/companies({id})/salesDocuments", method: "GET", description: "All sales documents in the Sales Import Journal.", category: "salesdocs" },
|
|
42
|
+
{ keywords: ["processed sales document", "processed documents", "imported documents"], endpoint: "/companies({id})/salesDocuments()", method: "GET", description: "Sales documents that have been processed into BC.", category: "salesdocs" },
|
|
43
|
+
{ keywords: ["sales order", "sales orders", "order list"], endpoint: "/companies({id})/salesOrders", method: "GET", description: "Sales orders.", category: "salesdocs" },
|
|
44
|
+
{ keywords: ["sales invoice", "sales invoices", "unposted invoice"], endpoint: "/companies({id})/salesInvoices", method: "GET", description: "Unposted sales invoices.", category: "salesdocs" },
|
|
45
|
+
{ keywords: ["credit memo", "sales credit memo", "credit memos"], endpoint: "/companies({id})/salesCreditMemos", method: "GET", description: "Unposted credit memos.", category: "salesdocs" },
|
|
46
|
+
{ keywords: ["return order", "sales return", "return orders"], endpoint: "/companies({id})/salesReturnOrders", method: "GET", description: "Sales return orders (API-created).", category: "salesdocs" },
|
|
47
|
+
{ keywords: ["create return order", "post return order", "new return"], endpoint: "/companies({id})/salesReturnOrders", method: "POST", description: "Create a return order.", category: "salesdocs" },
|
|
48
|
+
{ keywords: ["create sales document", "create order", "create invoice", "post sales document"], endpoint: "/companies({id})/salesDocuments", method: "POST", description: "Create a sales document (Quote/Order/Invoice/Credit Memo/Blanket Order/Return Order).", category: "salesdocs" },
|
|
49
|
+
{ keywords: ["batch sales document", "sales batch", "$batch", "bulk insert"], endpoint: "/companies({id})/$batch", method: "POST", description: "OData $batch sales document creates.", category: "salesdocs" },
|
|
50
|
+
// postedsales
|
|
51
|
+
{ keywords: ["posted sales", "posted document", "posted documents"], endpoint: "/companies({id})/postedSalesDocuments", method: "GET", description: "Union of posted invoices + credit memos.", category: "postedsales" },
|
|
52
|
+
{ keywords: ["posted invoice", "posted sales invoice"], endpoint: "/companies({id})/postedSalesInvoices", method: "GET", description: "Posted sales invoices.", category: "postedsales" },
|
|
53
|
+
{ keywords: ["posted credit memo", "posted credit"], endpoint: "/companies({id})/postedSalesCreditMemos", method: "GET", description: "Posted credit memos.", category: "postedsales" },
|
|
54
|
+
{ keywords: ["return receipt", "posted return", "return receipts"], endpoint: "/companies({id})/postedSalesReturnReceipts", method: "GET", description: "Posted return receipts.", category: "postedsales" },
|
|
55
|
+
{ keywords: ["shipment", "posted shipment", "shipments", "tracking lines"], endpoint: "/companies({id})/postedSalesShipments", method: "GET", description: "Posted shipments with tracking lines.", category: "postedsales" },
|
|
56
|
+
// exported
|
|
57
|
+
{ keywords: ["exported", "exported document", "mark exported", "is exported"], endpoint: "/companies({id})/exportedDocuments", method: "GET/POST/DELETE", description: "Track which docs have been exported to exclude them from future polls.", category: "exported" },
|
|
58
|
+
// metadata
|
|
59
|
+
{ keywords: ["trimit metadata", "$metadata trimit", "edmx trimit", "schema trimit"], endpoint: "/api/trimit/integration/v1.1/$metadata", method: "GET", description: "EDMX schema for the TRIMIT integration API.", category: "metadata" },
|
|
60
|
+
];
|
|
61
|
+
const CATEGORY_DESCRIPTIONS = {
|
|
62
|
+
standard: "Standard Microsoft Business Central API endpoints — companies, items, customers, OData metadata, entity definitions. Required for discovering companyId GUIDs.",
|
|
63
|
+
masterdata: "TRIMIT master data — campaigns, customer price groups, price group parameters, VarDim combinations/types/values, item attributes, collections.",
|
|
64
|
+
products: "Products — TRIMIT Masters (style headers), Items (SKUs), curated Products feed, categories.",
|
|
65
|
+
inventory: "Inventory — warehouse locations and per-(item,location) availability (incl. Future Delivers).",
|
|
66
|
+
customers: "TRIMIT-enriched customer resource (CRUD), contacts, salespersons.",
|
|
67
|
+
salesdocs: "Open sales document lifecycle — list/create Quote, Order, Invoice, Credit Memo, Blanket Order, Return Order; $batch for bulk insert.",
|
|
68
|
+
postedsales: "Posted sales documents — invoices, credit memos, return receipts, shipments with tracking.",
|
|
69
|
+
exported: "Exported-document markers used to exclude already-processed sales docs from future GETs.",
|
|
70
|
+
metadata: "TRIMIT integration API EDMX/$metadata for client codegen.",
|
|
71
|
+
};
|
|
72
|
+
const CATEGORY_TOOL_MAP = {
|
|
73
|
+
standard: standardTools,
|
|
74
|
+
masterdata: masterdataTools,
|
|
75
|
+
products: productsTools,
|
|
76
|
+
inventory: inventoryTools,
|
|
77
|
+
customers: customersTools,
|
|
78
|
+
salesdocs: salesdocsTools,
|
|
79
|
+
postedsales: postedsalesTools,
|
|
80
|
+
exported: exportedTools,
|
|
81
|
+
metadata: metadataTools,
|
|
82
|
+
};
|
|
83
|
+
export class TrimitMcpServer {
|
|
84
|
+
mcpServer;
|
|
85
|
+
registry;
|
|
86
|
+
categoryHandles = new Map();
|
|
87
|
+
constructor() {
|
|
88
|
+
this.registry = new ToolRegistry();
|
|
89
|
+
this.mcpServer = new McpServer({ name: "trimit-dev-assistant", version: "0.1.0" }, {
|
|
90
|
+
capabilities: {
|
|
91
|
+
tools: { listChanged: true },
|
|
92
|
+
},
|
|
93
|
+
instructions: "Use this server whenever the user is building, debugging, or learning about the TRIMIT API — the fashion/apparel ERP extension layered on Microsoft Dynamics 365 Business Central (api.businesscentral.dynamics.com). " +
|
|
94
|
+
"It documents 47 endpoints across master data, products, inventory, customers, sales documents, posted sales, exported markers, and metadata.\n\n" +
|
|
95
|
+
"WORKFLOW — choose the fastest path:\n" +
|
|
96
|
+
"1. Intent is clear → call load_category directly:\n" +
|
|
97
|
+
" - companies / standard BC API / metadata schema → load_category('standard')\n" +
|
|
98
|
+
" - campaigns / price groups / vardim / attributes / collections → load_category('masterdata')\n" +
|
|
99
|
+
" - masters / items / products / categories → load_category('products')\n" +
|
|
100
|
+
" - inventory / stock / locations → load_category('inventory')\n" +
|
|
101
|
+
" - customers / contacts / salespersons → load_category('customers')\n" +
|
|
102
|
+
" - sales orders / invoices / credit memos / quotes / return orders / $batch → load_category('salesdocs')\n" +
|
|
103
|
+
" - posted invoices / posted credit memos / posted shipments / return receipts → load_category('postedsales')\n" +
|
|
104
|
+
" - mark a doc exported / un-mark exported → load_category('exported')\n" +
|
|
105
|
+
" - TRIMIT $metadata for client codegen → load_category('metadata')\n" +
|
|
106
|
+
"2. Intent is ambiguous → call search_trimit_api to find the right category, then load it.\n" +
|
|
107
|
+
"3. Conceptual questions → call the relevant trimit_explain_* tool directly:\n" +
|
|
108
|
+
" - OAuth 2.0 / Entra / tokens → trimit_explain_auth\n" +
|
|
109
|
+
" - Base URLs / tenant / environment / companyId / placeholder substitution → trimit_explain_base_urls\n" +
|
|
110
|
+
" - $filter / $select / $expand / $orderby → trimit_explain_odata\n" +
|
|
111
|
+
" - $batch / bulk inserts → trimit_explain_batch\n" +
|
|
112
|
+
" - If-Match / ETag / IEEE754Compatible / concurrency → trimit_explain_concurrency\n" +
|
|
113
|
+
" - @odata.nextLink / pagination → trimit_explain_paging\n" +
|
|
114
|
+
" - Sales document lifecycle / processed vs exported → trimit_explain_doc_lifecycle\n" +
|
|
115
|
+
" - Errors / 401 / 403 / 409 / 412 → trimit_explain_errors\n\n" +
|
|
116
|
+
"This server constructs TRIMIT API requests (endpoint, method, headers, body, code example, auth) — it does not execute them.",
|
|
117
|
+
});
|
|
118
|
+
this.registerBootstrapTools();
|
|
119
|
+
}
|
|
120
|
+
registerBootstrapTools() {
|
|
121
|
+
// list_categories
|
|
122
|
+
this.mcpServer.tool("list_categories", "List all TRIMIT API resource categories with descriptions and load status. Categories: standard (Microsoft BC standard API), masterdata (campaigns/price groups/vardim/attributes/collections), products (masters/items/products feed), inventory (locations/availability), customers (CRUD/contacts/salespersons), salesdocs (orders/invoices/credit memos/$batch), postedsales (posted invoices/shipments/return receipts), exported (exported-doc markers), metadata ($metadata EDMX).", {}, async () => {
|
|
123
|
+
const categories = Object.entries(CATEGORY_DESCRIPTIONS).map(([name, description]) => ({
|
|
124
|
+
name,
|
|
125
|
+
description,
|
|
126
|
+
loaded: this.registry.loadedCategories.has(name),
|
|
127
|
+
toolCount: CATEGORY_TOOL_MAP[name]?.length ?? 0,
|
|
128
|
+
}));
|
|
129
|
+
return {
|
|
130
|
+
content: [
|
|
131
|
+
{
|
|
132
|
+
type: "text",
|
|
133
|
+
text: JSON.stringify({ categories }, null, 2),
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
// load_category
|
|
139
|
+
this.mcpServer.tool("load_category", "Activate TRIMIT API tools for a category. Call as soon as intent is clear — don't wait for confirmation. " +
|
|
140
|
+
"Categories: 'standard' (BC standard API), 'masterdata' (campaigns/price groups/vardim/attributes/collections), " +
|
|
141
|
+
"'products' (masters/items/products/categories), 'inventory' (locations/availability), 'customers' (TRIMIT customers CRUD + contacts/salespersons), " +
|
|
142
|
+
"'salesdocs' (open sales documents + $batch), 'postedsales' (posted invoices/credit memos/return receipts/shipments), " +
|
|
143
|
+
"'exported' (exported-doc markers), 'metadata' ($metadata). Multiple categories can be loaded simultaneously.", { category: z.string().describe("Category to load") }, async ({ category }) => {
|
|
144
|
+
const normalizedCategory = category.toLowerCase().trim();
|
|
145
|
+
if (!CATEGORY_TOOL_MAP[normalizedCategory]) {
|
|
146
|
+
const available = Object.keys(CATEGORY_TOOL_MAP).join(", ");
|
|
147
|
+
return {
|
|
148
|
+
content: [
|
|
149
|
+
{
|
|
150
|
+
type: "text",
|
|
151
|
+
text: JSON.stringify({
|
|
152
|
+
error: `Unknown category '${normalizedCategory}'. Available: ${available}`,
|
|
153
|
+
}),
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (this.registry.loadedCategories.has(normalizedCategory)) {
|
|
159
|
+
const toolNames = this.registry.getAll()
|
|
160
|
+
.filter((t) => t.category === normalizedCategory)
|
|
161
|
+
.map((t) => t.name);
|
|
162
|
+
return {
|
|
163
|
+
content: [
|
|
164
|
+
{
|
|
165
|
+
type: "text",
|
|
166
|
+
text: JSON.stringify({
|
|
167
|
+
message: `Category '${normalizedCategory}' is already loaded.`,
|
|
168
|
+
tools: toolNames,
|
|
169
|
+
}),
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const tools = CATEGORY_TOOL_MAP[normalizedCategory];
|
|
175
|
+
const newToolNames = this.registry.registerCategory(normalizedCategory, tools);
|
|
176
|
+
const handles = this.registerCategoryTools(normalizedCategory, tools);
|
|
177
|
+
this.categoryHandles.set(normalizedCategory, handles);
|
|
178
|
+
this.mcpServer.sendToolListChanged();
|
|
179
|
+
return {
|
|
180
|
+
content: [
|
|
181
|
+
{
|
|
182
|
+
type: "text",
|
|
183
|
+
text: JSON.stringify({
|
|
184
|
+
message: `Loaded ${newToolNames.length} tools for category '${normalizedCategory}'.`,
|
|
185
|
+
tools: newToolNames,
|
|
186
|
+
}),
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
// search_trimit_api
|
|
192
|
+
this.mcpServer.tool("search_trimit_api", "Search TRIMIT API endpoints by keyword. Covers masters/items/products, inventory, customers, contacts, salespersons, sales documents (quotes/orders/invoices/credit memos/return orders), posted sales (invoices/credit memos/return receipts/shipments), exported-doc markers, campaigns, price groups, vardim, attributes, collections, and standard BC endpoints (companies/items/customers/metadata/entity definitions).", { query: z.string().describe("Search terms, e.g. 'create order', 'inventory by location', 'mark exported', 'patch customer'") }, async ({ query }) => {
|
|
193
|
+
const lowerQuery = query.toLowerCase();
|
|
194
|
+
const queryWords = lowerQuery.split(/\s+/);
|
|
195
|
+
const scored = TRIMIT_API_SEARCH_MAP
|
|
196
|
+
.map((entry) => {
|
|
197
|
+
const score = entry.keywords.reduce((acc, keyword) => {
|
|
198
|
+
if (lowerQuery.includes(keyword))
|
|
199
|
+
return acc + 2;
|
|
200
|
+
const keywordWords = keyword.split(/\s+/);
|
|
201
|
+
const matchCount = keywordWords.filter((kw) => queryWords.some((qw) => qw.includes(kw) || kw.includes(qw))).length;
|
|
202
|
+
return acc + matchCount;
|
|
203
|
+
}, 0);
|
|
204
|
+
return { ...entry, score };
|
|
205
|
+
})
|
|
206
|
+
.filter((r) => r.score > 0)
|
|
207
|
+
.sort((a, b) => b.score - a.score)
|
|
208
|
+
.slice(0, 5);
|
|
209
|
+
if (scored.length === 0) {
|
|
210
|
+
return {
|
|
211
|
+
content: [
|
|
212
|
+
{
|
|
213
|
+
type: "text",
|
|
214
|
+
text: JSON.stringify({
|
|
215
|
+
message: "No results. Try keywords like: company, master, item, product, inventory, customer, order, invoice, credit memo, posted, batch, exported, campaign, vardim, attribute, collection, metadata.",
|
|
216
|
+
results: [],
|
|
217
|
+
}),
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const suggestedCategory = scored[0].category;
|
|
223
|
+
const results = scored.map(({ score: _score, ...rest }) => rest);
|
|
224
|
+
return {
|
|
225
|
+
content: [
|
|
226
|
+
{
|
|
227
|
+
type: "text",
|
|
228
|
+
text: JSON.stringify({
|
|
229
|
+
query,
|
|
230
|
+
suggestedCategory,
|
|
231
|
+
hint: `Call load_category with '${suggestedCategory}' to get tools for this resource area.`,
|
|
232
|
+
results,
|
|
233
|
+
}, null, 2),
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
};
|
|
237
|
+
});
|
|
238
|
+
// trimit_explain_auth
|
|
239
|
+
this.mcpServer.tool("trimit_explain_auth", "Explain TRIMIT API authentication — Microsoft Entra (Azure AD) OAuth 2.0 client credentials flow against Business Central.", {}, async () => {
|
|
240
|
+
const text = `# TRIMIT API Authentication
|
|
241
|
+
|
|
242
|
+
TRIMIT lives on top of Microsoft Dynamics 365 Business Central. **All authentication goes through Microsoft Entra (Azure AD)**, not TRIMIT directly.
|
|
243
|
+
|
|
244
|
+
## Flow: OAuth 2.0 Client Credentials (service-to-service)
|
|
245
|
+
|
|
246
|
+
\`\`\`
|
|
247
|
+
POST https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token
|
|
248
|
+
Content-Type: application/x-www-form-urlencoded
|
|
249
|
+
|
|
250
|
+
grant_type=client_credentials
|
|
251
|
+
&client_id={clientId}
|
|
252
|
+
&client_secret={clientSecret}
|
|
253
|
+
&scope=https://api.businesscentral.dynamics.com/.default
|
|
254
|
+
\`\`\`
|
|
255
|
+
|
|
256
|
+
Response includes \`access_token\`. Send it on every API call:
|
|
257
|
+
|
|
258
|
+
\`\`\`
|
|
259
|
+
Authorization: Bearer {token}
|
|
260
|
+
\`\`\`
|
|
261
|
+
|
|
262
|
+
## Prerequisites
|
|
263
|
+
1. **Azure AD app registration** in the customer's tenant
|
|
264
|
+
2. **Admin consent** for the Microsoft Graph + Business Central app permissions the integration needs
|
|
265
|
+
3. **Business Central setup**: in the BC client → **Microsoft Entra Applications** page → register the Entra app's client_id and assign it a **D365 BUS PREMIUM** or matching permission set
|
|
266
|
+
4. **TRIMIT integration license** enabled on the BC environment
|
|
267
|
+
|
|
268
|
+
## Tokens
|
|
269
|
+
- Default lifetime: ~1 hour
|
|
270
|
+
- Cache the token until ~5 min before expiry
|
|
271
|
+
- 401 'InvalidAuthenticationToken' usually means the token is expired or scoped to the wrong audience
|
|
272
|
+
|
|
273
|
+
## Tenant + Environment Substitution
|
|
274
|
+
- \`{tenant}\` — Entra tenant GUID (also used in the BC URL)
|
|
275
|
+
- \`{environment}\` — \`Production\`, \`Sandbox\`, or a custom env name created in the BC Admin Center
|
|
276
|
+
- \`{companyId}\` — BC company GUID; fetch with \`GET /api/v2.0/companies\` (use \`trimit_std_get_companies\`)
|
|
277
|
+
|
|
278
|
+
## Docs
|
|
279
|
+
- BC OAuth: https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/administration/authenticate-web-services-using-oauth
|
|
280
|
+
- TRIMIT integration overview: https://apidocs.trimit.com/`;
|
|
281
|
+
return { content: [{ type: "text", text }] };
|
|
282
|
+
});
|
|
283
|
+
// trimit_explain_base_urls
|
|
284
|
+
this.mcpServer.tool("trimit_explain_base_urls", "Explain the TRIMIT / Business Central base URL structure and the placeholders ({tenant}, {environment}, {companyId}) you must substitute.", {}, async () => {
|
|
285
|
+
const text = `# TRIMIT / Business Central Base URLs
|
|
286
|
+
|
|
287
|
+
All endpoints share the host \`api.businesscentral.dynamics.com\` under the customer's Entra tenant. There are three logical bases:
|
|
288
|
+
|
|
289
|
+
\`\`\`
|
|
290
|
+
TRIMIT integration API
|
|
291
|
+
${HOST}/{tenant}/{environment}/${TRIMIT_API_PATH}/companies({companyId})
|
|
292
|
+
|
|
293
|
+
Standard Microsoft BC API
|
|
294
|
+
${HOST}/{tenant}/{environment}/${STD_API_PATH}/companies({companyId})
|
|
295
|
+
|
|
296
|
+
BC OData v4 endpoint
|
|
297
|
+
${HOST}/{tenant}/{environment}/ODataV4
|
|
298
|
+
\`\`\`
|
|
299
|
+
|
|
300
|
+
## Placeholders
|
|
301
|
+
|
|
302
|
+
| Placeholder | What it is | Example |
|
|
303
|
+
|---|---|---|
|
|
304
|
+
| \`{tenant}\` | Entra tenant GUID | \`72f988bf-86f1-…\` |
|
|
305
|
+
| \`{environment}\` | BC environment name | \`Production\`, \`Sandbox\`, \`uat\` |
|
|
306
|
+
| \`{companyId}\` | BC company GUID | \`c0c0c0c0-…\` — get via GET /companies |
|
|
307
|
+
|
|
308
|
+
## Resolving {companyId}
|
|
309
|
+
Run \`trimit_std_get_companies\`. The response contains a \`value\` array with \`{ id, name, displayName }\` entries. Use the \`id\` GUID.
|
|
310
|
+
|
|
311
|
+
## Versioning
|
|
312
|
+
- TRIMIT integration path is currently \`trimit/integration/v1.1\`. Older deployments expose \`v1.0\`.
|
|
313
|
+
- Standard BC API is currently \`api/v2.0\`.
|
|
314
|
+
- Some endpoints accept \`$schemaversion=1.0\` to lock the response schema.
|
|
315
|
+
|
|
316
|
+
## Sandbox vs Production
|
|
317
|
+
- Environment names are not case-sensitive in the URL but match what's shown in the BC Admin Center.
|
|
318
|
+
- Sandboxes can be created on demand and refreshed from production.`;
|
|
319
|
+
return { content: [{ type: "text", text }] };
|
|
320
|
+
});
|
|
321
|
+
// trimit_explain_odata
|
|
322
|
+
this.mcpServer.tool("trimit_explain_odata", "Explain OData query parameters supported by the TRIMIT / Business Central API: $filter, $select, $expand, $orderby, $top, $skip, $count.", {}, async () => {
|
|
323
|
+
const text = `# OData Query Parameters
|
|
324
|
+
|
|
325
|
+
The TRIMIT API responds with **OData v4 JSON**. Every list endpoint accepts the standard query options.
|
|
326
|
+
|
|
327
|
+
## $filter
|
|
328
|
+
\`\`\`
|
|
329
|
+
?$filter=number eq '20002036'
|
|
330
|
+
?$filter=startswith(displayName,'ACME')
|
|
331
|
+
?$filter=lastDateModified gt 2024-01-01T00:00:00Z
|
|
332
|
+
\`\`\`
|
|
333
|
+
|
|
334
|
+
## $select — Narrow Fields
|
|
335
|
+
\`\`\`
|
|
336
|
+
?$select=id,number,description
|
|
337
|
+
\`\`\`
|
|
338
|
+
|
|
339
|
+
## $expand — Inline Related Entities
|
|
340
|
+
\`\`\`
|
|
341
|
+
?$expand=defaultDimensions,priceGroupParameters
|
|
342
|
+
?$expand=salesOrderLines($expand=additionalFields),additionalFields
|
|
343
|
+
\`\`\`
|
|
344
|
+
Nested expand uses the OData v4 form \`parent($expand=child)\`.
|
|
345
|
+
|
|
346
|
+
## $orderby
|
|
347
|
+
\`\`\`
|
|
348
|
+
?$orderby=lastDateModified desc
|
|
349
|
+
\`\`\`
|
|
350
|
+
|
|
351
|
+
## $top / $skip — Pagination
|
|
352
|
+
\`\`\`
|
|
353
|
+
?$top=100&$skip=200
|
|
354
|
+
\`\`\`
|
|
355
|
+
BC defaults to a 20 000-row page cap. Use \`@odata.nextLink\` for server-driven paging — see \`trimit_explain_paging\`.
|
|
356
|
+
|
|
357
|
+
## $count
|
|
358
|
+
\`\`\`
|
|
359
|
+
?$count=true
|
|
360
|
+
\`\`\`
|
|
361
|
+
|
|
362
|
+
## $batch
|
|
363
|
+
POST a JSON envelope to \`/$batch\` to combine many requests. See \`trimit_explain_batch\`.
|
|
364
|
+
|
|
365
|
+
## $schemaversion
|
|
366
|
+
Some TRIMIT endpoints accept \`$schemaversion=1.0\` to pin the response schema across upgrades.
|
|
367
|
+
|
|
368
|
+
## Encoding
|
|
369
|
+
Always URL-encode \`$filter\` values (quotes, slashes, spaces). Use the literal \`'\` (apostrophe) around string values, and double single-quotes inside strings.
|
|
370
|
+
|
|
371
|
+
## Docs
|
|
372
|
+
- BC OData: https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/webservices/odata-web-services
|
|
373
|
+
- OData v4 spec: https://docs.oasis-open.org/odata/odata/v4.01/`;
|
|
374
|
+
return { content: [{ type: "text", text }] };
|
|
375
|
+
});
|
|
376
|
+
// trimit_explain_paging
|
|
377
|
+
this.mcpServer.tool("trimit_explain_paging", "Explain server-driven paging in the TRIMIT / BC OData API — @odata.nextLink iteration and the 20 000-row page cap.", {}, async () => {
|
|
378
|
+
const text = `# Pagination
|
|
379
|
+
|
|
380
|
+
BC OData uses **server-driven paging**. If more rows exist than fit in a page, the response contains an \`@odata.nextLink\` URL pointing to the next page. Page sizes default to **20 000 rows**.
|
|
381
|
+
|
|
382
|
+
## Iteration Pattern
|
|
383
|
+
|
|
384
|
+
\`\`\`javascript
|
|
385
|
+
async function getAllPages(initialUrl, token) {
|
|
386
|
+
const all = [];
|
|
387
|
+
let url = initialUrl;
|
|
388
|
+
while (url) {
|
|
389
|
+
const r = await fetch(url, { headers: { Authorization: \`Bearer \${token}\` } });
|
|
390
|
+
if (!r.ok) throw new Error(\`HTTP \${r.status}: \${await r.text()}\`);
|
|
391
|
+
const data = await r.json();
|
|
392
|
+
if (data.value) all.push(...data.value);
|
|
393
|
+
url = data['@odata.nextLink'] ?? null;
|
|
394
|
+
}
|
|
395
|
+
return all;
|
|
396
|
+
}
|
|
397
|
+
\`\`\`
|
|
398
|
+
|
|
399
|
+
## $top + $skip
|
|
400
|
+
Use these only for explicit pagination control. \`$top\` clamps the page size; \`$skip\` advances the cursor. Combined they let you implement a manual paginator, but \`@odata.nextLink\` is preferred for full sweeps.
|
|
401
|
+
|
|
402
|
+
## Delta sync
|
|
403
|
+
The TRIMIT API has **no /delta endpoint**. Use a \`lastDateModified gt <isoTimestamp>\` filter on entities that expose that field (campaigns, products, customers) to pull incremental changes.`;
|
|
404
|
+
return { content: [{ type: "text", text }] };
|
|
405
|
+
});
|
|
406
|
+
// trimit_explain_concurrency
|
|
407
|
+
this.mcpServer.tool("trimit_explain_concurrency", "Explain optimistic concurrency in the TRIMIT / BC OData API — @odata.etag, If-Match header, and IEEE754Compatible decimals.", {}, async () => {
|
|
408
|
+
const text = `# Concurrency & Decimal Handling
|
|
409
|
+
|
|
410
|
+
## ETag-Based Optimistic Concurrency
|
|
411
|
+
|
|
412
|
+
Every GET on a single entity returns an \`@odata.etag\` value:
|
|
413
|
+
|
|
414
|
+
\`\`\`json
|
|
415
|
+
{
|
|
416
|
+
"@odata.context": "…",
|
|
417
|
+
"@odata.etag": "W/\\\"JzQ0O…\\\"",
|
|
418
|
+
"id": "…"
|
|
419
|
+
}
|
|
420
|
+
\`\`\`
|
|
421
|
+
|
|
422
|
+
On **PATCH** or **DELETE** you must send that etag back as \`If-Match\`:
|
|
423
|
+
|
|
424
|
+
\`\`\`
|
|
425
|
+
PATCH /api/.../customers({id})
|
|
426
|
+
If-Match: W/"JzQ0O…"
|
|
427
|
+
\`\`\`
|
|
428
|
+
|
|
429
|
+
If the resource changed since you read it, BC returns **412 Precondition Failed** — re-GET to pick up the latest etag and retry.
|
|
430
|
+
|
|
431
|
+
## Unconditional Updates
|
|
432
|
+
|
|
433
|
+
Use \`If-Match: *\` to bypass the check entirely. Fine for integrations that own the data, dangerous for shared resources.
|
|
434
|
+
|
|
435
|
+
## IEEE754Compatible Header
|
|
436
|
+
|
|
437
|
+
\`\`\`
|
|
438
|
+
IEEE754Compatible: true
|
|
439
|
+
\`\`\`
|
|
440
|
+
|
|
441
|
+
When set, BC encodes decimals as JSON **strings** instead of numbers — preventing JavaScript precision loss for large or high-scale decimals (currency, quantities). Send it on PATCH bodies that include decimal fields, and on GETs whose response you'll round-trip.
|
|
442
|
+
|
|
443
|
+
## Body Format
|
|
444
|
+
|
|
445
|
+
PATCH bodies should be **partial** (only the fields you're changing). Sending \`null\` clears a field; omitting it leaves the field unchanged.`;
|
|
446
|
+
return { content: [{ type: "text", text }] };
|
|
447
|
+
});
|
|
448
|
+
// trimit_explain_batch
|
|
449
|
+
this.mcpServer.tool("trimit_explain_batch", "Explain BC OData $batch — combine many sales document inserts into a single round-trip.", {}, async () => {
|
|
450
|
+
const text = `# OData $batch (Business Central)
|
|
451
|
+
|
|
452
|
+
POST a JSON batch envelope to \`/$batch\` under any of the company-scoped base URLs:
|
|
453
|
+
|
|
454
|
+
\`\`\`
|
|
455
|
+
POST ${HOST}/{tenant}/{environment}/${TRIMIT_API_PATH}/companies({companyId})/$batch
|
|
456
|
+
Authorization: Bearer {token}
|
|
457
|
+
Content-Type: application/json
|
|
458
|
+
\`\`\`
|
|
459
|
+
|
|
460
|
+
## Request Body
|
|
461
|
+
|
|
462
|
+
\`\`\`json
|
|
463
|
+
{
|
|
464
|
+
"requests": [
|
|
465
|
+
{ "id": "1", "method": "POST", "url": "salesDocuments",
|
|
466
|
+
"headers": { "Content-Type": "application/json" },
|
|
467
|
+
"body": { "docType": "Order", "docNo": "SO1001", "sellToCustomerNo": "10000", "orderDate": "2026-05-18", "salesDocumentLines": [ ... ] } },
|
|
468
|
+
{ "id": "2", "method": "POST", "url": "salesDocuments",
|
|
469
|
+
"headers": { "Content-Type": "application/json" },
|
|
470
|
+
"body": { "docType": "Order", "docNo": "SO1002", ... } }
|
|
471
|
+
]
|
|
472
|
+
}
|
|
473
|
+
\`\`\`
|
|
474
|
+
|
|
475
|
+
\`url\` is **relative** to the batch base URL.
|
|
476
|
+
|
|
477
|
+
## dependsOn
|
|
478
|
+
|
|
479
|
+
Make a request wait for another to succeed:
|
|
480
|
+
|
|
481
|
+
\`\`\`json
|
|
482
|
+
{ "id": "2", "method": "POST", "url": "...", "dependsOn": ["1"] }
|
|
483
|
+
\`\`\`
|
|
484
|
+
|
|
485
|
+
## Response Shape
|
|
486
|
+
|
|
487
|
+
\`\`\`json
|
|
488
|
+
{
|
|
489
|
+
"responses": [
|
|
490
|
+
{ "id": "1", "status": 201, "headers": { ... }, "body": { "@odata.etag": "...", ... } },
|
|
491
|
+
{ "id": "2", "status": 409, "headers": { ... }, "body": { "error": { "code": "Internal_EntityWithSameKeyExists", ... } } }
|
|
492
|
+
]
|
|
493
|
+
}
|
|
494
|
+
\`\`\`
|
|
495
|
+
|
|
496
|
+
- Responses can arrive **out of order** — match by \`id\`.
|
|
497
|
+
- A failed sub-request **does not** fail the whole batch (unless framing is invalid).
|
|
498
|
+
- Throttling: each sub-request counts independently against BC quotas.
|
|
499
|
+
|
|
500
|
+
## When To Use
|
|
501
|
+
- Bulk sales document inserts (\`trimit_salesdocs_post_lines_batch\`)
|
|
502
|
+
- Mixed CRUD across multiple TRIMIT entities in one round-trip
|
|
503
|
+
- Reducing 401-retry overhead during token refreshes
|
|
504
|
+
|
|
505
|
+
## Docs
|
|
506
|
+
https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/webservices/odata-using-batch`;
|
|
507
|
+
return { content: [{ type: "text", text }] };
|
|
508
|
+
});
|
|
509
|
+
// trimit_build_batch
|
|
510
|
+
this.mcpServer.tool("trimit_build_batch", "Build a TRIMIT / BC OData $batch request body. Pass requests with method, relative url, body, and optional headers / dependsOn.", {
|
|
511
|
+
requests: z.array(z.object({
|
|
512
|
+
id: z.string().describe("Unique id within the batch"),
|
|
513
|
+
method: z.enum(["GET", "POST", "PATCH", "PUT", "DELETE"]),
|
|
514
|
+
url: z.string().describe("Relative URL, e.g. 'salesDocuments' or 'customers({id})'"),
|
|
515
|
+
body: z.record(z.string(), z.unknown()).optional(),
|
|
516
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
517
|
+
dependsOn: z.array(z.string()).optional(),
|
|
518
|
+
})).min(1).max(100),
|
|
519
|
+
}, async ({ requests }) => {
|
|
520
|
+
const transformed = requests.map((req) => {
|
|
521
|
+
const out = {
|
|
522
|
+
id: req.id,
|
|
523
|
+
method: req.method,
|
|
524
|
+
url: req.url,
|
|
525
|
+
};
|
|
526
|
+
if (req.body !== undefined)
|
|
527
|
+
out.body = req.body;
|
|
528
|
+
const needsCT = ["POST", "PATCH", "PUT"].includes(req.method) && req.body !== undefined;
|
|
529
|
+
if (req.headers || needsCT) {
|
|
530
|
+
out.headers = {
|
|
531
|
+
...(needsCT ? { "Content-Type": "application/json" } : {}),
|
|
532
|
+
...(req.headers ?? {}),
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
if (req.dependsOn?.length)
|
|
536
|
+
out.dependsOn = req.dependsOn;
|
|
537
|
+
return out;
|
|
538
|
+
});
|
|
539
|
+
const endpoint = `${trimitBase()}/$batch`;
|
|
540
|
+
return {
|
|
541
|
+
content: [
|
|
542
|
+
{
|
|
543
|
+
type: "text",
|
|
544
|
+
text: JSON.stringify({
|
|
545
|
+
endpoint,
|
|
546
|
+
method: "POST",
|
|
547
|
+
headers: buildHeaders(),
|
|
548
|
+
body: { requests: transformed },
|
|
549
|
+
description: `Batch ${requests.length} TRIMIT API request${requests.length === 1 ? "" : "s"} into a single HTTP call.`,
|
|
550
|
+
docsUrl: "https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/webservices/odata-using-batch",
|
|
551
|
+
auth: TRIMIT_AUTH,
|
|
552
|
+
notes: "Responses arrive in 'responses'; match by id. Individual failures don't fail the batch. Sub-request URLs are relative to the batch base.",
|
|
553
|
+
codeExample: `const response = await fetch('${endpoint}', {\n method: 'POST',\n headers: { 'Authorization': 'Bearer {token}', 'Content-Type': 'application/json' },\n body: JSON.stringify(${JSON.stringify({ requests: transformed }, null, 2)})\n});\nconst { responses } = await response.json();\nfor (const r of responses) console.log(\`Request \${r.id}: HTTP \${r.status}\`);`,
|
|
554
|
+
}, null, 2),
|
|
555
|
+
},
|
|
556
|
+
],
|
|
557
|
+
};
|
|
558
|
+
});
|
|
559
|
+
// trimit_explain_doc_lifecycle
|
|
560
|
+
this.mcpServer.tool("trimit_explain_doc_lifecycle", "Explain the TRIMIT sales document lifecycle — Sales Import Journal, processedDate, exportedDocuments markers, and posted-document tables.", {}, async () => {
|
|
561
|
+
const text = `# Sales Document Lifecycle
|
|
562
|
+
|
|
563
|
+
Sales documents pass through three states in TRIMIT:
|
|
564
|
+
|
|
565
|
+
## 1. Open (Sales Import Journal)
|
|
566
|
+
|
|
567
|
+
\`POST /salesDocuments\` writes a new doc into the **TRIMIT Sales Import Journal**. \`releaseDocument: true\` releases it in BC immediately.
|
|
568
|
+
|
|
569
|
+
\`\`\`
|
|
570
|
+
GET /salesDocuments
|
|
571
|
+
\`\`\`
|
|
572
|
+
Returns **all** open documents — both unprocessed and processed-but-not-yet-marked-exported.
|
|
573
|
+
|
|
574
|
+
## 2. Processed
|
|
575
|
+
|
|
576
|
+
When BC processes the import journal row (manually or by job queue), it sets \`processedDate\` on the doc. Filter for processed docs with:
|
|
577
|
+
|
|
578
|
+
\`\`\`
|
|
579
|
+
GET /salesDocuments()?$filter=(processedDate gt 0001-01-01)
|
|
580
|
+
\`\`\`
|
|
581
|
+
|
|
582
|
+
A processed doc may still appear in /salesDocuments until you explicitly mark it exported.
|
|
583
|
+
|
|
584
|
+
## 3. Posted
|
|
585
|
+
|
|
586
|
+
After invoicing / shipping in BC, the doc moves to one of the **Posted** tables:
|
|
587
|
+
|
|
588
|
+
| Action | Source table |
|
|
589
|
+
|---|---|
|
|
590
|
+
| Posted invoice | postedSalesInvoices |
|
|
591
|
+
| Posted credit memo | postedSalesCreditMemos |
|
|
592
|
+
| Posted shipment | postedSalesShipments |
|
|
593
|
+
| Posted return receipt | postedSalesReturnReceipts |
|
|
594
|
+
|
|
595
|
+
The unposted record may be deleted from /salesDocuments by BC after posting.
|
|
596
|
+
|
|
597
|
+
## exportedDocuments — Marker, Not State
|
|
598
|
+
|
|
599
|
+
\`POST /exportedDocuments\` writes a marker that **excludes the doc from future GET /salesDocuments calls** (and the analogous typed lists). It does **not** change anything in BC — it's purely a client-state optimization so integrations can avoid re-processing the same doc.
|
|
600
|
+
|
|
601
|
+
\`\`\`
|
|
602
|
+
POST /exportedDocuments { "type": "Order", "number": "SO1001" }
|
|
603
|
+
GET /exportedDocuments('Order','SO1001') → 200 if marked, 404 if not
|
|
604
|
+
DELETE /exportedDocuments('Order','SO1001') → 204
|
|
605
|
+
\`\`\`
|
|
606
|
+
|
|
607
|
+
\`documentType\` is case-sensitive. Accepted values include 'Quote', 'Order', 'Invoice', 'Credit Memo', 'Blanket Order', 'Return Order', 'Posted Invoice', 'Posted Credit Memo', 'Posted Shipment', 'Posted Return Receipt'.
|
|
608
|
+
|
|
609
|
+
## Recovery
|
|
610
|
+
If your downstream system loses a doc, DELETE the exported marker — the doc reappears in the next GET poll.`;
|
|
611
|
+
return { content: [{ type: "text", text }] };
|
|
612
|
+
});
|
|
613
|
+
// trimit_explain_errors
|
|
614
|
+
this.mcpServer.tool("trimit_explain_errors", "Explain common TRIMIT / Business Central API error responses and how to fix them.", {}, async () => {
|
|
615
|
+
const text = `# Common Errors
|
|
616
|
+
|
|
617
|
+
BC returns errors as an OData envelope:
|
|
618
|
+
|
|
619
|
+
\`\`\`json
|
|
620
|
+
{
|
|
621
|
+
"error": {
|
|
622
|
+
"code": "BadRequest_InvalidRequest",
|
|
623
|
+
"message": "…human readable message…"
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
\`\`\`
|
|
627
|
+
|
|
628
|
+
## 400 Bad Request
|
|
629
|
+
- Malformed JSON body, wrong field types, missing required keys
|
|
630
|
+
- For sales docs: invalid \`docType\` (must be exactly 'Order', 'Quote', 'Invoice', 'Credit Memo', 'Blanket Order', 'Return Order' — case-sensitive)
|
|
631
|
+
|
|
632
|
+
## 401 Unauthorized
|
|
633
|
+
- \`InvalidAuthenticationToken\` → expired/wrong-audience token
|
|
634
|
+
- Check audience: \`https://api.businesscentral.dynamics.com\`
|
|
635
|
+
- Check tenant in token endpoint matches the URL tenant
|
|
636
|
+
- Missing \`Authorization: Bearer …\` header
|
|
637
|
+
|
|
638
|
+
## 403 Forbidden
|
|
639
|
+
- BC permission set on the Entra app does not grant access to this entity
|
|
640
|
+
- TRIMIT integration license not enabled on the environment
|
|
641
|
+
|
|
642
|
+
## 404 Not Found
|
|
643
|
+
- Wrong path segment (TRIMIT vs Standard API)
|
|
644
|
+
- Wrong company GUID
|
|
645
|
+
- Resource id wrong / deleted
|
|
646
|
+
- For \`exportedDocuments(type,number)\` 404 means *the marker doesn't exist*, not that the doc doesn't exist
|
|
647
|
+
|
|
648
|
+
## 409 Conflict
|
|
649
|
+
- \`Internal_EntityWithSameKeyExists\` — duplicate primary key (e.g. customer number, sales doc number)
|
|
650
|
+
- Use a different number, or PATCH the existing record
|
|
651
|
+
|
|
652
|
+
## 412 Precondition Failed
|
|
653
|
+
- Stale \`If-Match\` etag — re-GET to refresh and retry the PATCH
|
|
654
|
+
|
|
655
|
+
## 429 Too Many Requests
|
|
656
|
+
BC's global throttling — back off and retry. Respect \`Retry-After\` if present.
|
|
657
|
+
|
|
658
|
+
## 5xx
|
|
659
|
+
Transient — retry with exponential backoff. Persistent 500s on a single record may indicate corrupt data in BC; surface to the BC admin.
|
|
660
|
+
|
|
661
|
+
## Debugging Tips
|
|
662
|
+
- Always log \`error.code\` and the response \`x-request-id\` / \`x-ms-correlation-request-id\` header — Microsoft support uses those for diagnosis
|
|
663
|
+
- For sales doc POSTs, the error body often includes per-line failure context — log the entire body
|
|
664
|
+
|
|
665
|
+
## Docs
|
|
666
|
+
https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/api-reference/v2.0/dynamics-error-codes`;
|
|
667
|
+
return { content: [{ type: "text", text }] };
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
registerCategoryTools(_categoryName, tools) {
|
|
671
|
+
const handles = [];
|
|
672
|
+
for (const tool of tools) {
|
|
673
|
+
const handle = this.mcpServer.tool(tool.name, tool.description, tool.zodShape, async (args) => {
|
|
674
|
+
try {
|
|
675
|
+
const result = tool.handler(args);
|
|
676
|
+
return {
|
|
677
|
+
content: [
|
|
678
|
+
{
|
|
679
|
+
type: "text",
|
|
680
|
+
text: JSON.stringify(result, null, 2),
|
|
681
|
+
},
|
|
682
|
+
],
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
catch (err) {
|
|
686
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
687
|
+
return {
|
|
688
|
+
content: [
|
|
689
|
+
{
|
|
690
|
+
type: "text",
|
|
691
|
+
text: JSON.stringify({ error: message }),
|
|
692
|
+
},
|
|
693
|
+
],
|
|
694
|
+
isError: true,
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
handles.push(handle);
|
|
699
|
+
}
|
|
700
|
+
return handles;
|
|
701
|
+
}
|
|
702
|
+
async start() {
|
|
703
|
+
const transport = new StdioServerTransport();
|
|
704
|
+
await this.mcpServer.connect(transport);
|
|
705
|
+
}
|
|
706
|
+
}
|