A TypeScript framework for building stateless MCP servers that can be deployed to serverless and edge environments.
Also known as vitemcp/server, @vitemcp/server
- Stars
- 22
- Tools
- 1
- Downloads, 30 days
- —
- Latest version
- —
- Last commit
- Nov 11, 2026
- License
- MIT
What is ViteMCP MCP?
ViteMCP is a TypeScript framework for building stateless MCP servers that can be deployed to serverless and edge environments. It exposes 1 tool, such as title.
Summary generated from the project's README
About ViteMCP MCP server
ViteMCP is a TypeScript framework designed for developers who need to build stateless MCP servers. It specifically targets the 2026-07-28 protocol revision, which removes the need for an initialize handshake or session IDs. This architecture makes the framework ideal for deployment in serverless and edge environments like Cloudflare Workers or Deno Deploy, as every request is self-contained.
The framework simplifies the development process by handling boilerplate tasks such as connection management, tool handling, and response formatting. Developers can define tools, resources, and prompts using the Standard Schema specification, allowing for integration with libraries like Zod, ArkType, or Valibot. It also supports generating entire servers from OpenAPI documents and provides built-in support for image and audio content. For testing, it includes an in-memory transport and a CLI to inspect and debug server functionality without binding to a local port.
What you can do with it
- 1Automating server creation from APIsYou can quickly build a complete server by providing an existing OpenAPI document.
Written from the project's README. Every use case is checked against a quote from it.
How to install ViteMCP MCP
Formats verified 10 Nov 2026Add to CursorAdd to GooseInstall ViteMCP MCP in Claude Code
$ claude mcp add server -- npx -y @vitemcp/serverInstall ViteMCP MCP in Cursor
~/.cursor/mcp.json
{
"mcpServers": {
"server": {
"command": "npx",
"args": [
"-y",
"@vitemcp/server"
]
}
}
}Install ViteMCP MCP in VS Code
.vscode/mcp.json
{
"servers": {
"server": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"@vitemcp/server"
]
}
}
}Install ViteMCP MCP in VS Code CLI
$ code --add-mcp '{"name":"server","type":"stdio","command":"npx","args":["-y","@vitemcp/server"]}'Install ViteMCP MCP in Claude Desktop
claude_desktop_config.json
{
"mcpServers": {
"server": {
"command": "npx",
"args": [
"-y",
"@vitemcp/server"
]
}
}
}Install ViteMCP MCP in Codex
$ codex mcp add server -- npx -y @vitemcp/serverInstall ViteMCP MCP in Codex config.toml
~/.codex/config.toml
[mcp_servers.server] command = "npx" args = ["-y", "@vitemcp/server"]
Install ViteMCP MCP in Gemini CLI
$ gemini mcp add -s user server npx -- -y @vitemcp/serverInstall ViteMCP MCP in Gemini settings.json
~/.gemini/settings.json
{
"mcpServers": {
"server": {
"command": "npx",
"args": [
"-y",
"@vitemcp/server"
]
}
}
}ViteMCP MCP tools 1
Detected from the READMEtitle
| title | — |
Configuration
YOUR_ENV_VAR
| Name | Description | |
|---|---|---|
| YOUR_ENV_VAR | — |
Automated checks
Ran Nov 12, 2026Full report →Passed our automated checks. That is not a security audit: read the code and give the server the narrowest access it needs. How checks work
- Source repository linked and reachablePass
- Open source license (OSI)Pass
- Commit or release in the last yearPass
- Package name not close to a popular packagePass
- Remote endpoint uses HTTPSNot checked
- Destructive tools are labelledPass
- No hidden instructions to the modelPass
- No critical advisory in the package or its dependenciesNot checked
README
View on GitHubViteMCP
A TypeScript framework for building stateless MCP servers.
[!IMPORTANT]
ViteMCP targets MCP revision 2026-07-28, which made the protocol stateless. There is no
initializehandshake and noMcp-Session-Id: every request is self-contained and carries its own protocol version and client capabilities.If you are coming from a session-based version, see Migrating from the session-based API.
Contents
- Features
- When to use ViteMCP over the official SDK?
- Installation
- Quickstart
- Core Concepts
- Server Features
- Deployment
- Testing and Debugging
- FAQ
- Showcase
- Acknowledgements
Authentication has a dedicated reference — see the OAuth guide. So does OpenAPI conversion — see the OpenAPI guide.
Features
- Simple Tool, Resource, Prompt definition
- Authentication
- Per-request auth context
- Image content
- Audio content
- Embedded
- Error handling
- HTTP Streaming
- HTTPS Support for secure connections
- Custom HTTP routes for REST APIs, webhooks, and admin interfaces
- Edge Runtime Support for Cloudflare Workers, Deno Deploy, and more
- Stateless by construction — every request stands alone, so serverless just works
- CORS (enabled by default)
- Progress notifications
- Multi round-trip requests for asking the client for more input
- Prompt argument auto-completion
- Cacheable list results (
ttlMs/cacheScope) - Health-check endpoint
- Generate a whole server from an OpenAPI document
- In-memory transport for unit testing without binding a port
- CLI for testing and debugging
When to use ViteMCP over the official SDK?
ViteMCP is built on top of the official SDK.
The official SDK provides foundational blocks for building MCPs, but leaves many implementation details to you — all of which src/ViteMCP.ts handles on your behalf:
- Initiating and configuring all the server components
- Handling of connections
- Handling of tools
- Handling of responses
- Handling of resources
- Adding prompts, resources and resource templates
- Embedding resource, image and audio content blocks
ViteMCP eliminates this complexity by providing an opinionated framework that:
- Handles all the boilerplate automatically
- Provides simple, intuitive APIs for common tasks
- Includes built-in best practices and error handling
- Lets you focus on your MCP's core functionality
When to choose ViteMCP: You want to build MCP servers quickly without dealing with low-level implementation details.
When to use the official SDK: You need maximum control or have specific architectural requirements. In this case, we encourage referencing ViteMCP's implementation to avoid common pitfalls.
Installation
npm install @vitemcp/server
Quickstart
[!NOTE]
There are many real-world examples of using ViteMCP in the wild. See the Showcase for examples.
import { ViteMCP } from "@vitemcp/server";
import { z } from "zod"; // Or any validation library that supports Standard Schema
const server = new ViteMCP({
name: "My Server",
version: "1.0.0",
});
server.addTool({
name: "add",
description: "Add two numbers",
parameters: z.object({
a: z.number(),
b: z.number(),
}),
execute: async (args) => {
return String(args.a + args.b);
},
});
server.start({
transportType: "stdio",
});
That's it! You have a working MCP server.
You can test the server in terminal with:
git clone https://github.com/vitemcp/server.git
cd server
pnpm install
pnpm build
# Test the addition server example using CLI:
npx @vitemcp/server dev src/examples/addition.ts
# Test the addition server example using MCP Inspector:
npx @vitemcp/server inspect src/examples/addition.ts
If you are looking for something to start from, src/examples/ has runnable servers covering tools, authentication, custom routes and edge deployment.
Core Concepts
Tools
Tools in MCP allow servers to expose executable functions that can be invoked by clients and used by LLMs to perform actions.
ViteMCP uses the Standard Schema specification for defining tool parameters. This allows you to use your preferred schema validation library (like Zod, ArkType, or Valibot) as long as it implements the spec.
Zod Example:
import { z } from "zod";
server.addTool({
name: "fetch-zod",
description: "Fetch the content of a url (using Zod)",
parameters: z.object({
url: z.string(),
}),
execute: async (args) => {
return await fetchWebpageContent(args.url);
},
});
ArkType Example:
import { type } from "arktype";
server.addTool({
name: "fetch-arktype",
description: "Fetch the content of a url (using ArkType)",
parameters: type({
url: "string",
}),
execute: async (args) => {
return await fetchWebpageContent(args.url);
},
});
Valibot Example:
Valibot requires the peer dependency @valibot/to-json-schema.
import * as v from "valibot";
server.addTool({
name: "fetch-valibot",
description: "Fetch the content of a url (using Valibot)",
parameters: v.object({
url: v.string(),
}),
execute: async (args) => {
return await fetchWebpageContent(args.url);
},
});
Plain JSON Schema Example:
If you already have a JSON Schema — from an OpenAPI document, a config file, or
another server — jsonSchemaAdapter wraps it so it can be used directly, with
no schema library in between.
It requires the peer dependency ajv, which does the validation, plus
ajv-formats if your schema uses format keywords such as email or uri.
Both are imported the first time a tool is called, so servers that don't use
this pay nothing for it.
npm install ajv ajv-formats
import { jsonSchemaAdapter } from "@vitemcp/server";
server.addTool({
name: "fetch-json-schema",
description: "Fetch the content of a url (using plain JSON Schema)",
parameters: jsonSchemaAdapter({
type: "object",
properties: {
url: { type: "string", format: "uri" },
},
required: ["url"],
}),
execute: async (args) => {
const { url } = args as { url: string };
return await fetchWebpageContent(url);
},
});
Works for outputSchema too, and is advertised as written — as are Zod and
ArkType schemas, which emit their own JSON Schema. Where one of those holds
something JSON Schema cannot express — a z.date(), a z.bigint(), an ArkType
narrow() — that part alone is advertised as open, as {} or as the type a
narrow() narrows, and the library still enforces it. A schema from any other
library (Valibot, for one) is converted to JSON Schema for tools/list only;
arguments and results are still validated by the library itself, so a
v.transform() reaches execute and a v.check() is enforced even though
JSON Schema can express neither. Converted input schemas get
additionalProperties: false on every object, except where a record or rest
schema (v.record(), v.objectWithRest()) describes the extra keys, or where
the keys are declared in the branches of a v.union(), v.variant() or
v.intersect(), or behind the $ref a v.lazy() becomes; converted output
schemas are left as they are. A tool whose schema cannot be converted at all,
or whose parameters do not describe an object, is left out of the server and
reported through the logger.
A plain JSON Schema is validated in the dialect its $schema declares —
2020-12, 2019-09, draft-07 or draft-06 — and as 2020-12 when it declares none,
which is how MCP clients read it.
Unlike the schema libraries above, a plain JSON Schema carries no TypeScript
types, so execute receives unknown arguments. Cast or narrow them yourself.
Tools Without Parameters
When creating tools that don't require parameters, you have two options:
-
Omit the parameters property entirely:
server.addTool({ name: "sayHello", description: "Say hello", // No parameters property execute: async () => { return "Hello, world!"; }, }); -
Explicitly define empty parameters:
import { z } from "zod"; server.addTool({ name: "sayHello", description: "Say hello", parameters: z.object({}), // Empty object execute: async () => { return "Hello, world!"; }, });
[!NOTE]
Both approaches are fully compatible with all MCP clients, including Cursor. ViteMCP automatically generates the proper schema in both cases.
Structured Tool Output
Tools can declare an outputSchema and return structured data. ViteMCP exposes that value as MCP structuredContent, while also returning a JSON text fallback for clients that only render text content.
server.addTool({
name: "get-weather",
description: "Get weather for a city",
parameters: z.object({
city: z.string(),
}),
outputSchema: z.object({
temperature: z.number(),
humidity: z.number(),
}),
execute: async ({ city }) => {
const weather = await getWeather(city);
return {
temperature: weather.temperature,
humidity: weather.humidity,
};
},
});
You can also return explicit text content and structured content together:
server.addTool({
name: "get-weather",
description: "Get weather for a city",
parameters: z.object({
city: z.string(),
}),
outputSchema: z.object({
temperature: z.number(),
humidity: z.number(),
}),
execute: async ({ city }) => {
const weather = await getWeather(city);
return {
content: [
{
type: "text",
text: `${city}: ${weather.temperature}F`,
},
],
structuredContent: {
temperature: weather.temperature,
humidity: weather.humidity,
},
};
},
});
When outputSchema is provided, ViteMCP validates structuredContent before sending the tool result. Invalid structured output is returned to the client as a tool error instead of silently violating the advertised schema.
ViteMCP tells a structured payload apart from MCP content blocks by shape, so a payload that looks like one of the other shapes needs saying outright. Return it under structuredContent and there is nothing left to infer — the JSON text fallback is still written for you:
server.addTool({
name: "get-document",
parameters: z.object({ id: z.string() }),
outputSchema: z.object({
title: z.string(),
content: z.array(z.string()),
}),
execute: async ({ id }) => {
const doc = await getDocument(id);
return {
structuredContent: { title: doc.title, content: doc.paragraphs },
};
},
});
Three payload shapes need it: an array-valued content field (as above), a resultType of "input_required", and a lone structuredContent field of your own. Everything else is inferred correctly — including a payload that has a structuredContent field among others. In particular a top-level type field is safe — including type: "text" and the other content-block kinds — because a declared outputSchema turns off the single-content-block shorthand.
The wrapper must stand alone: { structuredContent }, optionally with isError or _meta. An object carrying anything else is read as a payload in its own right, so keys execute meant to keep never reach the client.
Restricting who can call a tool
A tool's optional canAccess receives the request's auth context and returns whether the caller may use it. Tools it rejects are filtered out of tools/list entirely. A caller who may use none of them gets an empty list rather than an error.
server.addTool({
name: "admin-tool",
description: "An admin-only tool",
canAccess: (auth) => auth?.role === "admin",
execute: async () => "Welcome, admin!",
});
Built-in helpers — requireAuth, requireScopes, requireRole, requireAll, requireAny — cover the usual cases; see Tool Authorization.
Return types
execute may return a plain string, a content object, or an array of content
blocks. A bare string is shorthand for a single text block — these two are
equivalent:
execute: async () => "Hello, world!";
execute: async () => ({ content: [{ type: "text", text: "Hello, world!" }] });
| Return value | Produces |
|---|---|
"some string" |
one text block |
{ content: [...] } |
any mix of text, image, audio and resource blocks |
imageContent({ ... }) |
one image block |
audioContent({ ... }) |
one audio block |
imageContent and audioContent build a block from a url, a path, or a
buffer — exactly one of the three. Both accept timeoutMs to bound a URL
download (30 seconds by default).
import { audioContent, imageContent } from "@vitemcp/server";
server.addTool({
name: "fetch-image",
description: "Fetch an image",
parameters: z.object({ url: z.string() }),
execute: async (args) => imageContent({ url: args.url }),
// ...or imageContent({ path: "/path/to/image.png" })
// ...or imageContent({ buffer: Buffer.from(base64Png, "base64") })
});
Each helper returns a single block, so combine them under content to send
more than one:
execute: async () => ({
content: [
{ type: "text", text: "Here is what I found:" },
await imageContent({ url: "https://example.com/image.png" }),
await audioContent({ url: "https://example.com/audio.mp3" }),
],
});
Raw blocks work too when you already hold base64 data:
execute: async () => ({
content: [
{ type: "image", data: base64Png, mimeType: "image/png" },
{ type: "audio", data: base64Mp3, mimeType: "audio/mpeg" },
],
});
Tool Annotations
Tools can include annotations that provide richer context and control by adding metadata about a tool's behavior:
server.addTool({
name: "fetch-content",
description: "Fetch content from a URL",
parameters: z.object({
url: z.string(),
}),
annotations: {
title: "Web Content Fetcher", // Human-readable title for UI display
readOnlyHint: true, // Tool doesn't modify its environment
openWorldHint: true, // Tool interacts with external entities
},
execute: async (args) => {
return await fetchWebpageContent(args.url);
},
});
The available annotations are:
| Annotation | Type | Default | Description |
|---|---|---|---|
title |
string | - | A human-readable title for the tool, useful for UI display |
readOnlyHint |
boolean | false |
If true, indicates the tool does not modify its environment |
destructiveHint |
boolean | true |
If true, the tool may perform destructive updates (only meaningful when readOnlyHint is false) |
idempotentHint |
boolean | false |
If true, calling the tool repeatedly with the same arguments has no additional effect (only meaningful when readOnlyHint is false) |
openWorldHint |
boolean | true |
If true, the tool may interact with an "open world" of external entities |
These annotations help clients and LLMs better understand how to use the tools and what to expect when calling them.
Resources
Resources represent any kind of data that an MCP server wants to make available to clients. This can include:
- File contents
- Screenshots and images
- Log files
- And more
Each resource is identified by a unique URI and can contain either text or binary data.
server.addResource({
uri: "file:///logs/app.log",
name: "Application Logs",
mimeType: "text/plain",
async load() {
return {
text: await readLogFile(),
};
},
});
[!NOTE]
loadcan return multiple resources. This could be used, for example, to return a list of files inside a directory when the directory is read.async load() { return [ { text: "First file content", }, { text: "Second file content", }, ]; }
You can also return binary contents in load:
async load() {
return {
blob: 'base64-encoded-data'
};
}
load receives a context object mirroring what tool.execute gets — auth, log, requestId and the multi-round-trip helpers. reportProgress is not included, since it is tied to a tool call's progress token:
server.addResource({
uri: "file:///logs/app.log",
name: "Application Logs",
mimeType: "text/plain",
async load(context) {
context.log.info("loading application logs", { requestedBy: auth?.userId });
return {
text: await readLogFile(),
};
},
});
Resource templates
You can also define resource templates:
server.addResourceTemplate({
uriTemplate: "file:///logs/{name}.log",
name: "Application Logs",
mimeType: "text/plain",
arguments: [
{
name: "name",
description: "Name of the log",
required: true,
},
],
async load({ name }) {
return {
text: `Example log content for ${name}`,
};
},
});
Like plain resources, load also receives auth and context as its second and third arguments (see Resources).
Resource template argument auto-completion
Provide complete functions for resource template arguments to enable automatic completion:
server.addResourceTemplate({
uriTemplate: "file:///logs/{name}.log",
name: "Application Logs",
mimeType: "text/plain",
arguments: [
{
name: "name",
description: "Name of the log",
required: true,
complete: async (value) => {
if (value === "Example") {
return {
values: ["Example Log"],
};
}
return {
values: [],
};
},
},
],
async load({ name }) {
return {
text: `Example log content for ${name}`,
};
},
});
Embedded Resources
ViteMCP provides a convenient embedded() method that simplifies including resources in tool responses. This feature reduces code duplication and makes it easier to reference resources from within tools.
Basic Usage
server.addTool({
name: "get_user_data",
description: "Retrieve user information",
parameters: z.object({
userId: z.string(),
}),
execute: async (args) => {
return {
content: [
{
type: "resource",
resource: await server.embedded(`user://profile/${args.userId}`),
},
],
};
},
});
Working with Resource Templates
The embedded() method works seamlessly with resource templates. A URI reaches
the same template, with the same arguments, as it would through
resources/read:
// Define a resource template
server.addResourceTemplate({
uriTemplate: "docs://project/{section}",
name: "Project Documentation",
mimeType: "text/markdown",
arguments: [
{
name: "section",
required: true,
},
],
async load(args) {
const docs = {
"getting-started": "# Getting Started\n\nWelcome to our project!",
"api-reference": "# API Reference\n\nAuthentication is required.",
};
return {
text: docs[args.section] || "Documentation not found",
};
},
});
// Use embedded resources in a tool
server.addTool({
name: "get_documentation",
description: "Retrieve project documentation",
parameters: z.object({
section: z.enum(["getting-started", "api-reference"]),
}),
execute: async (args) => {
return {
content: [
{
type: "resource",
resource: await server.embedded(`docs://project/${args.section}`),
},
],
};
},
});
Working with Direct Resources
It also works with directly defined resources:
// Define a direct resource
server.addResource({
uri: "system://status",
name: "System Status",
mimeType: "text/plain",
async load() {
return {
text: "System operational",
};
},
});
// Use in a tool
server.addTool({
name: "get_system_status",
description: "Get current system status",
parameters: z.object({}),
execute: async () => {
return {
content: [
{
type: "resource",
resource: await server.embedded("system://status"),
},
],
};
},
});
ViteMCP MCP questions
How do I add ViteMCP MCP to Claude Code?
Run claude mcp add server -- npx -y @vitemcp/server in your terminal. Add --scope user to make it available in every project.
How do I add ViteMCP MCP to Cursor?
Open ~/.cursor/mcp.json and add the server under mcpServers, using the Cursor config shown in the install section above.
What tools does ViteMCP MCP provide?
It exposes 1 tool, including title.
How to use with Claude Desktop?
The README includes a specific FAQ section for Claude Desktop, though the detailed answer is not provided in the snippet.
Can I generate a server from an OpenAPI document?
Yes, ViteMCP allows you to generate an entire server from an OpenAPI document.
What makes ViteMCP different from the official SDK?
ViteMCP is an opinionated framework built on top of the official SDK that handles boilerplate like connection management, tool handling, and resource embedding.