Développement/Outils unifiés

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

FichierRôle
convex/tools/types.tsdefineTool et contrat ToolDefinition
convex/tools/schema.tsConversion Zod vers JSON Schema
convex/tools/registry.tsRegistre unique et détection des doublons
convex/tools/routes.tsAlias REST, correspondance et détection des collisions
convex/tools/definitions/*.tools.tsOutils regroupés par domaine
convex/tools/actions.tslistAvailableTools, getToolSchema, executeTool
convex/http.tsRoutes REST sous /api/v1
src/lib/mcp/server.tsAdaptateur MCP utilisant le même registre
scripts/tools-cli.mjsAdaptateur 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

ChampRôle
nameNom unique en snake_case
descriptionDescription destinée au LLM
categoryRegroupement utilisé par pnpm tools list --category
accessread ou write, utilisé par les indices MCP
inputSchemaObjet z.object(...) dont les champs peuvent utiliser .describe()
routeAlias REST facultatif
handlerReç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.

  • path est relatif à /api/v1 et peut contenir des segments :param. /tools/* est réservé à la découverte.
  • Un outil représente une opération et une seule méthode.
  • GET et DELETE construisent l’entrée avec le chemin et la query string ; POST, PATCH et PUT utilisent le chemin et le corps JSON.
  • Les valeurs de chemin et de query string sont des chaînes ; utilisez z.coerce.number() ou z.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.