Skip to Content

Setting up the endpoints

Preparations

As weโ€™re going to use function calling weโ€™re going to change the target of the generated JSON schemas to draft-7, as LLMs currently support only this version.

src/lib/withZod.ts
404: Not Found

The code above is fetched from GitHub repository.ย 

For the sake of shorter code, we also need to create a reusable constant BASE_FIELDS that is going to be used to omit id, entityType, createdAt and updatedAt fields from the Zod models, but also BASE_KEYS array that contains the keys of these fields to use with lodash.omit. This will help us to build proper create/update Zod models and omit these fields from entity objects when we need to create input objects.

src/constants.ts
import type { BaseEntity } from './types'; export const BASE_FIELDS = { id: true, entityType: true, createdAt: true, updatedAt: true, } as const satisfies { readonly [key in keyof BaseEntity]: true }; export const BASE_KEYS = Object.keys(BASE_FIELDS) as (keyof BaseEntity)[];

For example, hereโ€™s how the UpdateUserSchema can be created by omitting the base fields from the generated UserSchema:

import { UserSchema } from '@schemas/index'; import { BASE_FIELDS } from '@/constants'; const UpdateUserSchema = UserSchema.omit(BASE_FIELDS); // fullName and email fields only

Implementing controllers and services

The controllers are going to declare full CRUD operations for User and Task entities, implemented as follows:

  • Each method is decorated with @operation decorator that describes the operation, including summary, description, and custom x-tool-successMessage and x-tool-errorMessage properties for MCP responses.
  • Each method is created with withZod function that implements request validation and schema emission.
  • Each method uses sessionGuard decorator to protect the endpoints, as described in the authentication article.
  • The โ€œget allโ€ endpoints use x-tool-disable operation option to exclude them from being used as tools by AI. The tools are going to use search endpoints instead to simulate more realistic scenarios.

The services are going to implement the actual business logic for each controller method, including database operations using Prisma client and embedding generation using OpenAI embeddings.

  • Database requests are invoked using DatabaseService.prisma where the prisma property is a normal Prisma client instance with extensionsย .
    • Deletion operations return __isDeleted property to help the front-end to reconcile the state properly for soft deletions, see State page. The property is added by the Prisma client extension when DatabaseService.prisma.xxx.delete methods are invoked.
    • In order to trigger database events (see Polling page) on task deletions, tasks are deleted explicitly, even though we have ON DELETE CASCADE set up in the database schema. This is required if the app also uses Polling for updates made by other users or external systems, such as MCP server or Telegram bot.
  • Creations and updates are followed by EmbeddingService.generateEntityEmbedding calls and the search endpoints use EmbeddingService.vectorSearch function to perform vector search using OpenAI embeddings and pgvector.
src/modules/user/UserController.ts
import { procedure, prefix, get, put, post, del, operation } from "vovk"; import { z } from "zod"; import { TaskSchema, UserSchema } from "@schemas/index"; import { sessionGuard } from "@/decorators/sessionGuard"; import { BASE_FIELDS } from "@/constants"; import UserService from "./UserService"; @prefix("users") export default class UserController { @operation({ summary: "Get all users", description: "Retrieves a list of all users.", "x-tool-disable": true, // Make it to be used as an procedure only, excluding from the list of available tools }) @get() @sessionGuard() static getUsers = procedure({ output: UserSchema.array(), handle: UserService.getUsers, }); @operation({ summary: "Find users by ID, full name, or email", description: "Retrieves users that match the provided ID, full name, or email. Used to search the users when they need to be updated or deleted.", "x-tool-successMessage": "Users found successfully", }) @get("search") @sessionGuard() static findUsers = procedure({ query: z.object({ search: z.string().meta({ description: "Search term for users", examples: ["john.doe", "Jane"], }), }), output: UserSchema.array(), handle: ({ vovk }) => UserService.findUsers(vovk.query().search), }); @operation({ summary: "Create user", description: "Creates a new user with the provided details.", "x-tool-successMessage": "User created successfully", }) @post() @sessionGuard() static createUser = procedure({ body: UserSchema.omit(BASE_FIELDS), output: UserSchema, handle: async ({ vovk }) => UserService.createUser(await vovk.body()), }); @operation({ summary: "Update user", description: "Updates an existing user with the provided details, such as their email or name.", "x-tool-successMessage": "User updated successfully", }) @put("{id}") @sessionGuard() static updateUser = procedure({ body: UserSchema.omit(BASE_FIELDS).partial(), params: UserSchema.pick({ id: true }), output: UserSchema, handle: async ({ vovk }) => UserService.updateUser(vovk.params().id, await vovk.body()), }); @operation({ summary: "Delete user", description: "Deletes a user by ID.", "x-tool-successMessage": "User deleted successfully", }) @del("{id}") @sessionGuard() static deleteUser = procedure({ params: UserSchema.pick({ id: true }), output: UserSchema.partial().extend({ __isDeleted: z.literal(true), tasks: TaskSchema.partial() .extend({ __isDeleted: z.literal(true) }) .array(), }), handle: async ({ vovk }) => UserService.deleteUser(vovk.params().id), }); }

The code above is fetched from GitHub repository.ย 

Last updated on