6f54d765c4
La reponse embarquait la definition EasyBuilder complete, au-dela du seuil de
rejet du client MCP (~70 000 caracteres, D24). Deux parametres de fenetre au
schema (D23) : max_data_chars (defaut 20 000) et data_offset (defaut 0). Les
metadonnees du workflow restent completes dans chaque tranche ; seul `data`
est fenetre, et dataTotalChars est porte par toute reponse.
La tranche est verbatim -- decoupe de chaine, rien d'autre. Ne jamais resumer
ni parser ce blob : la concatenation des tranches doit reconstituer la
definition a l'octet pres. Verifie : 20 000 + 20 000 + 20 000 + 11 512 =
71 512, concatenation identique au blob d'origine (premiers et derniers
caracteres compris).
Mesures avant/apres (protocole, LIMAGRAIN, longueur de content[0].text) :
StackerCrane_LocationIsAccessibleByExtractor_PR 79 092 -> 23 117
(blob data : 71 512, desormais annonce par dataTotalChars)
CST_SendRejectContainersToPK (CustomApp) 101 816 -> 23 023
(blob data : 92 362)
StackerCrane_LoadMovementForOutboundTask_PR 10 587 -> 10 652
(blob de 9 013 : sous le defaut, objet workflow identique a l'octet pres,
ni truncated ni hint -- les +65 caracteres sont les trois champs de
fenetre, contrat "total toujours porte" de D24)
Gardes de valeur dans le code de l'outil, pas dans le wrapper (D23 ne valide
que les noms) : data_offset: -5 et max_data_chars: 1.5 echouent avant tout
appel reseau avec un message nommant l'attendu.
Baseline preservee : 23 outils, 6 resources.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
281 lines
11 KiB
JavaScript
281 lines
11 KiB
JavaScript
/**
|
|
* Workflow Tools
|
|
* MCP tools for searching and retrieving workflow information via API
|
|
*/
|
|
|
|
const workflowService = require('../services/workflow-service');
|
|
|
|
// Taille par défaut d'une tranche du blob `data` de get_workflow_details.
|
|
// Ordre de grandeur cible de D24 (~20-25 000 caractères par réponse) : avec
|
|
// l'échappement JSON et les métadonnées, 20 000 caractères de blob tiennent
|
|
// sous ~23 000 caractères de réponse.
|
|
const DEFAULT_MAX_DATA_CHARS = 20000;
|
|
|
|
/**
|
|
* List available workflow tools
|
|
*/
|
|
function listTools() {
|
|
return [
|
|
{
|
|
name: 'search_workflows',
|
|
description: 'Search workflows by name. Returns matching workflows with metadata. Workflows are lazy-loaded from API on first request and cached for 1 hour, per application. Client-specific workflows (CST_* prefix) live in the "CustomApp" application — pass application: "CustomApp" to search them.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
additionalProperties: false,
|
|
properties: {
|
|
query: {
|
|
type: 'string',
|
|
description: 'Search query (searches in workflow name)',
|
|
},
|
|
category: {
|
|
type: 'string',
|
|
description: 'Filter by the applicationName field of the returned workflows (workflows have no category field). Since `application` selects which application is fetched, all its workflows share the same applicationName — prefer `application` to change scope; `category` only narrows within the fetched set.',
|
|
},
|
|
limit: {
|
|
type: 'number',
|
|
description: 'Maximum results to return (default: 50)',
|
|
default: 50,
|
|
},
|
|
application: {
|
|
type: 'string',
|
|
description: 'AD application whose workflows are searched (default: the active profile\'s application, usually EasyWMS). Client-specific workflows live in "CustomApp". Full list via list_workflow_categories.',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: 'get_workflow_details',
|
|
description: `Get full details of a specific workflow by ID or name.
|
|
The EasyBuilder definition (the \`data\` blob) is large — 71 512 characters for a StackerCrane workflow, 92 362 for CST_SendRejectContainersToPK — so it is returned as a VERBATIM WINDOW (max_data_chars / data_offset). Workflow metadata is always complete; only \`data\` is windowed. dataTotalChars always carries the full blob size, and concatenating the slices in offset order reproduces the definition byte for byte.`,
|
|
inputSchema: {
|
|
type: 'object',
|
|
additionalProperties: false,
|
|
properties: {
|
|
workflow_id: {
|
|
type: 'string',
|
|
description: 'Workflow ID or exact name',
|
|
},
|
|
application: {
|
|
type: 'string',
|
|
description: 'AD application the workflow belongs to (default: the active profile\'s application, usually EasyWMS). Client-specific workflows (CST_*) live in "CustomApp".',
|
|
},
|
|
max_data_chars: {
|
|
type: 'number',
|
|
description: `Maximum number of characters of the \`data\` blob returned by this call (default: ${DEFAULT_MAX_DATA_CHARS}). The slice is verbatim — never summarised, reformatted or parsed. Pass 0 for metadata only.`,
|
|
default: DEFAULT_MAX_DATA_CHARS,
|
|
},
|
|
data_offset: {
|
|
type: 'number',
|
|
description: 'Character offset in the `data` blob where the returned slice starts (default: 0). When the response carries truncated: true, its hint gives the next offset to pass here.',
|
|
default: 0,
|
|
},
|
|
},
|
|
required: ['workflow_id'],
|
|
},
|
|
},
|
|
{
|
|
name: 'list_workflow_categories',
|
|
description: 'List the AD applications declared on the tenant (Application/GetAll) with their workflow counts where already loaded. Workflows have no category field — the application is the only grouping. Use the `application` parameter of the workflow/AD tools to query a specific one (e.g. "CustomApp" for client-specific CST_* workflows).',
|
|
inputSchema: {
|
|
type: 'object',
|
|
additionalProperties: false,
|
|
properties: {
|
|
application: {
|
|
type: 'string',
|
|
description: 'Load and count the workflows of this application (default: the active profile\'s application). Other applications are listed without loading them.',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Execute workflow tool
|
|
*/
|
|
async function executeTool(name, args) {
|
|
try {
|
|
switch (name) {
|
|
case 'search_workflows':
|
|
return await searchWorkflows(args);
|
|
|
|
case 'get_workflow_details':
|
|
return await getWorkflowDetails(args);
|
|
|
|
case 'list_workflow_categories':
|
|
return await listWorkflowCategories(args);
|
|
|
|
default:
|
|
throw new Error(`Unknown workflow tool: ${name}`);
|
|
}
|
|
} catch (error) {
|
|
console.error(`[WorkflowTools] Error executing ${name}:`, error.message);
|
|
return {
|
|
content: [{
|
|
type: 'text',
|
|
text: JSON.stringify({
|
|
success: false,
|
|
error: error.message,
|
|
tool: name
|
|
}, null, 2)
|
|
}],
|
|
isError: true
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tool: search_workflows
|
|
*/
|
|
async function searchWorkflows(args) {
|
|
const { query, category, limit = 50, application } = args;
|
|
|
|
console.error(`[WorkflowTools] Searching workflows: query="${query}", category="${category}", limit=${limit}, application=${application || '(profil)'}`);
|
|
|
|
const results = await workflowService.searchWorkflows(query, category, limit, application);
|
|
|
|
return {
|
|
content: [{
|
|
type: 'text',
|
|
text: JSON.stringify({
|
|
success: true,
|
|
...(application ? { application } : {}),
|
|
count: results.length,
|
|
// Clés réelles de l'API AD (minuscules, cf. D5) : id, name, version,
|
|
// applicationName, commonInfo. Pas de code/category/description.
|
|
workflows: results.map(w => {
|
|
const commonInfo = w.commonInfo || w.CommonInfo || {};
|
|
return {
|
|
id: w.id || w.Id,
|
|
name: w.name || w.Name,
|
|
applicationName: w.applicationName || w.ApplicationName,
|
|
version: w.version || w.Version,
|
|
createdBy: commonInfo.createdBy,
|
|
createDate: commonInfo.createDate,
|
|
updateDate: commonInfo.updateDate
|
|
};
|
|
})
|
|
}, null, 2)
|
|
}]
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Garde de valeur des paramètres de fenêtre. Le wrapper D23 valide les noms de
|
|
* paramètres, pas les valeurs — la garde vit donc ici, avant tout appel réseau.
|
|
*/
|
|
function assertWindowValue(value, fallback, paramName) {
|
|
if (value == null) return fallback;
|
|
if (!Number.isInteger(value) || value < 0) {
|
|
const attendu = paramName === 'max_data_chars'
|
|
? `taille max de la tranche du blob data, défaut ${DEFAULT_MAX_DATA_CHARS}, 0 = métadonnées seules`
|
|
: 'offset de départ dans le blob data, défaut 0';
|
|
throw new Error(
|
|
`${paramName} invalide : ${JSON.stringify(value)}. Attendu : un entier >= 0 (${attendu}).`
|
|
);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Tool: get_workflow_details
|
|
*
|
|
* La définition EasyBuilder (blob `data`) fait à elle seule 71 512 caractères
|
|
* sur un StackerCrane et 92 362 sur CST_SendRejectContainersToPK : la réponse
|
|
* complète dépassait le seuil de rejet du client MCP (D24). On renvoie une
|
|
* TRANCHE VERBATIM du blob (découpe de chaîne, rien d'autre) : les métadonnées
|
|
* restent complètes, et concaténer les tranches dans l'ordre des offsets
|
|
* reconstitue la définition à l'octet près. Ne jamais résumer ni « parser » ce
|
|
* blob pour n'en renvoyer que des morceaux jugés utiles.
|
|
*/
|
|
async function getWorkflowDetails(args) {
|
|
const { workflow_id, application, max_data_chars, data_offset } = args;
|
|
|
|
const maxDataChars = assertWindowValue(max_data_chars, DEFAULT_MAX_DATA_CHARS, 'max_data_chars');
|
|
const dataOffset = assertWindowValue(data_offset, 0, 'data_offset');
|
|
|
|
console.error(`[WorkflowTools] Getting workflow details: ${workflow_id} (application: ${application || '(profil)'}, max_data_chars=${maxDataChars}, data_offset=${dataOffset})`);
|
|
|
|
const workflow = await workflowService.getWorkflowDetails(workflow_id, application);
|
|
|
|
const payload = { success: true, workflow };
|
|
|
|
// Seul un blob `data` textuel se fenêtre ; un workflow sans définition (ou
|
|
// d'une forme inattendue) sort inchangé.
|
|
if (typeof workflow?.data === 'string') {
|
|
const total = workflow.data.length;
|
|
const slice = workflow.data.slice(dataOffset, dataOffset + maxDataChars);
|
|
const nextOffset = dataOffset + slice.length;
|
|
|
|
payload.workflow = { ...workflow, data: slice };
|
|
// La taille totale est portée par TOUTE réponse : truncated se vérifie
|
|
// depuis la réponse elle-même (D24).
|
|
payload.dataTotalChars = total;
|
|
payload.dataOffset = dataOffset;
|
|
payload.returned = slice.length;
|
|
|
|
if (nextOffset < total) {
|
|
payload.truncated = true;
|
|
payload.hint =
|
|
`Blob \`data\` tronqué : ${slice.length} caractère(s) sur ${total} renvoyé(s) depuis l'offset ${dataOffset}. ` +
|
|
`Rappelez get_workflow_details avec les mêmes workflow_id/application et data_offset: ${nextOffset} pour la tranche ` +
|
|
`suivante (max_data_chars change la taille des tranches). Les tranches sont verbatim : les concaténer dans l'ordre ` +
|
|
`des offsets reconstitue la définition EasyBuilder à l'octet près.`;
|
|
}
|
|
}
|
|
|
|
return {
|
|
content: [{
|
|
type: 'text',
|
|
text: JSON.stringify(payload, null, 2)
|
|
}]
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Tool: list_workflow_categories
|
|
*/
|
|
async function listWorkflowCategories(args) {
|
|
const { application } = args || {};
|
|
|
|
console.error(`[WorkflowTools] Listing applications (workflow groupings), application=${application || '(profil)'}`);
|
|
|
|
// La liste vient d'Application/GetAll (9 applications sur le tenant mesuré),
|
|
// pas des applicationName du seul cache actif (D26). Seule l'application
|
|
// demandée (ou celle du profil) est chargée — pas de préchargement des
|
|
// autres (D10) : leurs comptes n'apparaissent que si déjà en cache.
|
|
const applications = await workflowService.fetchApplications();
|
|
const stats = await workflowService.getWorkflowStats(application);
|
|
const cacheStatus = workflowService.getCacheStatus();
|
|
|
|
const enriched = applications.map(a => ({
|
|
name: a.name,
|
|
version: a.version,
|
|
...(cacheStatus[a.name]
|
|
? { workflowCount: cacheStatus[a.name].count, cacheAge: cacheStatus[a.name].age }
|
|
: { workflowCount: null }),
|
|
}));
|
|
|
|
return {
|
|
content: [{
|
|
type: 'text',
|
|
text: JSON.stringify({
|
|
success: true,
|
|
note: 'Workflows have no category field in the AD API — the application is the only grouping. workflowCount is only known for applications already loaded (lazy loading); pass application to search_workflows/get_ad_elements to load one.',
|
|
totalApplications: applications.length,
|
|
applications: enriched,
|
|
loaded: {
|
|
application: stats.application,
|
|
totalWorkflows: stats.total,
|
|
cacheAge: stats.cacheAge
|
|
}
|
|
}, null, 2)
|
|
}]
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
listTools,
|
|
executeTool,
|
|
};
|