Outils unifiés
Déclarez une fonctionnalité une fois et exposez-la par REST, MCP et la CLI.
Un outil est une capacité nommée et validée par schéma. Déclaré une fois dans Convex, il est servi par trois adaptateurs afin que REST, MCP et la CLI restent alignés.
Organisation
| Fichier | Rôle |
|---|---|
convex/tools/types.ts | defineTool et contrat ToolDefinition |
convex/tools/schema.ts | Conversion Zod vers JSON Schema |
convex/tools/registry.ts | Registre unique et détection des doublons |
convex/tools/routes.ts | Alias REST, correspondance et détection des collisions |
convex/tools/definitions/*.tools.ts | Outils regroupés par domaine |
convex/tools/actions.ts | listAvailableTools, getToolSchema, executeTool |
convex/http.ts | Routes REST sous /api/v1 |
src/lib/mcp/server.ts | Adaptateur MCP utilisant le même registre |
scripts/tools-cli.mjs | Adaptateur CLI (pnpm tools) |
Ajouter un outil
Déclarez-le avec defineTool. Son schéma d’entrée est un objet Zod validé avant l’exécution du handler, quel que soit l’adaptateur.
// convex/tools/definitions/project.tools.ts
import { z } from "zod";
import { internal } from "@convex/_generated/api";
import { defineTool } from "@convex/tools/types";
export const projectTools = [
defineTool({
name: "get_project",
description: "Récupère un projet de l’organisation par son identifiant.",
category: "organization",
access: "read",
inputSchema: z.object({
projectId: z.string().min(1).describe("Identifiant du projet"),
}),
handler: async (input, { ctx, organizationId }) => {
const project = await ctx.runQuery(
internal.projects.queries.getForOrgApi,
{ organizationId, projectId: input.projectId },
);
if (!project) {
return {
success: false,
code: "not_found",
error: `Projet "${input.projectId}" introuvable dans cette organisation`,
};
}
return { success: true, data: { project } };
},
}),
];Enregistrez-le ensuite dans convex/tools/registry.ts :
import { projectTools } from "@convex/tools/definitions/project.tools";
const definitions: RegisteredTool[] = [
...organizationTools,
...memberTools,
...billingTools,
...projectTools,
];L’outil apparaît alors dans GET /api/v1/tools, pnpm tools list et la liste MCP.
Contrat
| Champ | Rôle |
|---|---|
name | Nom unique en snake_case |
description | Description destinée au LLM |
category | Regroupement utilisé par pnpm tools list --category |
access | read ou write, utilisé par les indices MCP |
inputSchema | Objet z.object(...) dont les champs peuvent utiliser .describe() |
route | Alias REST facultatif |
handler | Reçoit l’entrée validée et { ctx, organizationId, source } |
Le handler renvoie un résultat discriminé :
type ToolResult<TData> =
| { success: true; data: TData }
| { success: false; error: string; code?: string };code: "not_found" correspond à HTTP 404 ; les autres échecs correspondent à 400.
Alias REST
Chaque outil est accessible sur POST /api/v1/tools/<name>. Ajoutez route pour lui donner une URL plus lisible :
defineTool({
name: "get_project",
// ...
route: { method: "GET", path: "/projects/:projectId" },
handler: async (input, { ctx, organizationId }) => {
/* ... */
},
});GET /api/v1/projects/prj_123 exécute alors le même outil via executeTool. Aucun second handler n’est nécessaire et la route apparaît dans la découverte.
pathest relatif à/api/v1et peut contenir des segments:param./tools/*est réservé à la découverte.- Un outil représente une opération et une seule méthode.
GETetDELETEconstruisent l’entrée avec le chemin et la query string ;POST,PATCHetPUTutilisent le chemin et le corps JSON.- Les valeurs de chemin et de query string sont des chaînes ; utilisez
z.coerce.number()ouz.coerce.boolean(). - Deux outils partageant la même méthode et la même forme de chemin provoquent une erreur au chargement.
Les alias REST renvoient directement le résultat. Les points /api/v1/tools/* conservent l’enveloppe { "data": … } attendue par MCP et la CLI.
Autorisation
Les outils ne vérifient pas eux-mêmes l’authentification. Toutes les interfaces passent par executeTool, qui valide l’identifiant et résout organizationId avant le handler. Une clé API ou un jeton OAuth sont vérifiés dans Convex ; un outil write exige la portée nowstack.write.
L’adaptateur MCP lit les métadonnées du registre, puis exécute l’outil via POST /api/v1/tools/:name avec l’en-tête x-tool-source: mcp. Autorisation, validation et logique métier restent centralisées.
Source
source indique l’interface appelante (api, mcp ou cli). Utilisez-la pour les statistiques ou la limitation de débit, jamais pour l’autorisation.