Files
mcp-wms-api/src/tools/metadata-tools.js
T
Arthur Ria a1acb781b0 L5.3 : champ tool dans toutes les enveloppes d'erreur locales
Convention 3 impose { success: false, error, tool }, mais le champ tool n'etait
ajoute que par le wrapper de src/index.js. Les enveloppes construites
localement dans src/tools/ ne le portaient pas : call_query_api avec
query_type: 7 repondait { success: false, error: "query_type invalide : ..." },
sans tool. Le contrat etait donc respecte ou non selon le chemin d'erreur --
alors qu'il est lu par Claude, pas par un humain.

Balayage des 8 modules : 15 enveloppes success:false au total, 11 y ont gagne
le champ tool (ad-tools, config-tools, wms-query-tools et workflow-tools
l'avaient deja via le catch de leur executeTool). Les champs additionnels sont
conserves et passent apres tool : warning de resolution d'api-tools,
availableCount + hint de get_entity_metadata, listes de profils de
profile-tools.

Grep de controle -- 15 enveloppes, 0 sans tool :

  src/tools/ad-tools.js:140            tool: name
  src/tools/api-tools.js:170           tool: 'call_query_api'
  src/tools/api-tools.js:217           tool: 'execute_command'
  src/tools/config-tools.js:179        tool: 'get_system_parameters'
  src/tools/log-tools.js:116           tool: 'list_log_files'
  src/tools/log-tools.js:157           tool: 'read_recent_logs'
  src/tools/log-tools.js:227           tool: 'search_logs'
  src/tools/metadata-tools.js:110      tool: 'get_entity_metadata'
  src/tools/metadata-tools.js:154      tool: 'get_entity_metadata'
  src/tools/metadata-tools.js:197      tool: 'generic_search'
  src/tools/profile-tools.js:111       tool: 'get_current_wms_profile'
  src/tools/profile-tools.js:129       tool: 'switch_wms_profile'
  src/tools/profile-tools.js:159       tool: 'switch_wms_profile'
  src/tools/wms-query-tools.js:169     tool: name
  src/tools/workflow-tools.js:117      tool: name

Verifie en execution (protocole, LIMAGRAIN sauf mention) -- 11 enveloppes
declenchees, toutes avec tool :

  call_query_api query_type: 7          -> tool: call_query_api
  get_entity_metadata entite inconnue   -> tool + availableCount + hint
  query_wms_entities entite inconnue    -> tool: query_wms_entities
  get_workflow_details id inconnu       -> tool: get_workflow_details
  get_ad_elements type inconnu          -> tool: get_ad_elements
  switch_wms_profile profil inconnu     -> tool + profiles
  read_recent_logs fichier inexistant   -> tool: read_recent_logs
  list_log_files / search_logs /
    read_recent_logs sur EUROTRAFIC     -> tool, garde SaaS D9 (sans reseau)

Trois catch restent couverts statiquement, faute de declencheur : celui
d'execute_command (interdit d'appel, il ecrit dans le WMS), celui de
generic_search (l'API tolere categorie inexistante comme limit negative :
elle repond success), et celui de get_current_wms_profile (inatteignable tant
qu'un profil par defaut se charge au demarrage).

Baseline preservee : 23 outils, 6 resources.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 15:16:28 +02:00

205 lines
5.9 KiB
JavaScript

/**
* Metadata & GenericSearch Tools
* - get_entity_metadata : list queryable entities and their properties (Metadata API)
* - generic_search : full-text search across indexed WMS documents (GenericSearch API)
*/
const apiService = require('../services/api-service').getInstance();
function listTools() {
return [
{
name: 'get_entity_metadata',
description: `Get metadata for WMS entities — field names, types, and assembly info — without querying actual data.
Use this to discover the exact field names and types for any entity before building a filter.
- Without entity_name: lists all queryable entities
- With entity_name (partial match ok, case-insensitive): returns field names + types for that entity`,
inputSchema: {
type: 'object',
additionalProperties: false,
properties: {
entity_name: {
type: 'string',
description: 'Partial entity name to look up (e.g. "OutboundOrder", "Task", "Stock"). Omit to list all entities.',
},
},
},
},
{
name: 'generic_search',
description: `Full-text search across indexed WMS documents (faster than LINQ for keyword searches).
First call without arguments to get available categories, then search within them.
Examples:
- generic_search(query="ORD-2024-001") — find an order by code
- generic_search(query="picking error", categories=["Tasks"]) — search tasks
- generic_search() — list available search categories`,
inputSchema: {
type: 'object',
additionalProperties: false,
properties: {
query: {
type: 'string',
description: 'Search keyword or phrase',
},
categories: {
type: 'array',
items: { type: 'string' },
description: 'Document categories to search (omit to search all). Get available categories by calling without query.',
},
limit: {
type: 'number',
description: 'Max results (default: 20)',
default: 20,
},
skip: {
type: 'number',
description: 'Offset for pagination (default: 0)',
default: 0,
},
},
},
},
];
}
async function executeTool(name, args) {
switch (name) {
case 'get_entity_metadata':
return await getEntityMetadata(args);
case 'generic_search':
return await genericSearch(args);
default:
throw new Error(`Unknown metadata tool: ${name}`);
}
}
async function getEntityMetadata(args) {
const { entity_name } = args || {};
try {
const entities = await apiService.getMetadataEntities();
if (!entity_name) {
// Return full entity list
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
totalEntities: Array.isArray(entities) ? entities.length : '?',
entities,
}, null, 2),
}],
};
}
// Find matching entities (case-insensitive partial match)
const search = entity_name.toLowerCase();
const matches = Array.isArray(entities)
? entities.filter(e => {
const name = (e.FullName || e.Name || e.fullName || e.name || JSON.stringify(e)).toLowerCase();
return name.includes(search);
})
: [];
if (matches.length === 0) {
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: `No entity matching "${entity_name}" found`,
tool: 'get_entity_metadata',
availableCount: Array.isArray(entities) ? entities.length : '?',
hint: 'Call get_entity_metadata without entity_name to see all entities',
}, null, 2),
}],
isError: true,
};
}
// Fetch properties for each match (up to 3)
const results = [];
for (const entity of matches.slice(0, 3)) {
const assemblyFullName = entity.AssemblyFullName || entity.assemblyFullName;
const fullName = entity.FullName || entity.fullName;
let properties = null;
if (assemblyFullName && fullName) {
try {
properties = await apiService.getEntityProperties(assemblyFullName, fullName);
} catch (err) {
console.error(`[Metadata] Could not fetch properties for ${fullName}: ${err.message}`);
}
}
results.push({ entity, properties });
}
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
query: entity_name,
matchCount: matches.length,
results,
}, null, 2),
}],
};
} catch (err) {
return {
content: [{
type: 'text',
text: JSON.stringify({ success: false, error: err.message, tool: 'get_entity_metadata' }, null, 2),
}],
isError: true,
};
}
}
async function genericSearch(args) {
const { query, categories = [], limit = 20, skip = 0 } = args || {};
try {
// No query: return available categories
if (!query) {
const cats = await apiService.getSearchCategories();
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
availableCategories: cats,
hint: 'Pass query + optional categories[] to search',
}, null, 2),
}],
};
}
const result = await apiService.genericSearch(query, categories, limit, skip);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
query,
categories: categories.length > 0 ? categories : 'all',
result,
}, null, 2),
}],
};
} catch (err) {
return {
content: [{
type: 'text',
text: JSON.stringify({ success: false, error: err.message, tool: 'generic_search' }, null, 2),
}],
isError: true,
};
}
}
module.exports = { listTools, executeTool };