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.
404: Not FoundThe 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.
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 onlyImplementing 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 customx-tool-successMessageandx-tool-errorMessageproperties for MCP responses. - Each method is created with
withZodfunction that implements request validation and schema emission. - Each method uses
sessionGuarddecorator to protect the endpoints, as described in the authentication article. - The โget allโ endpoints use
x-tool-disableoperation 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.prismawhere theprismaproperty is a normal Prisma client instance with extensionsย .- Deletion operations return
__isDeletedproperty 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 whenDatabaseService.prisma.xxx.deletemethods are invoked. - In order to trigger database events (see Polling page) on task deletions, tasks are deleted explicitly, even though we have
ON DELETE CASCADEset 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.
- Deletion operations return
- Creations and updates are followed by
EmbeddingService.generateEntityEmbeddingcalls and the search endpoints useEmbeddingService.vectorSearchfunction to perform vector search using OpenAI embeddings and pgvector.
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),
});
}