App Requirements
Everything mctx needs from your project to deploy your App. A clear checklist with examples.
mctx deploys your App (an MCP server) from a GitHub repository. Here is everything it needs from your project.
The complete checklist
Your repository needs three things:
- A
package.jsonwith name, version, description, and main fields - A built JavaScript file at the path specified by
main - A default export with a
fetchhandler (the Cloudflare Worker format)
That is it. No custom config files, no platform-specific setup. mctx reads standard package.json fields and auto-detects your App's capabilities at deploy time.
A working example
Here is a complete project that passes all validation. Start from this and modify it.
package.json:
{
"name": "weather-api",
"version": "1.0.0",
"description": "Get real-time weather data for any location worldwide",
"main": "dist/index.js",
"type": "module",
"scripts": {
"build": "esbuild src/index.js --bundle --minify --platform=node --format=esm --outfile=dist/index.js"
},
"dependencies": {
"@mctx-ai/app": "^0.3.0"
},
"devDependencies": {
"esbuild": "latest"
}
}
src/index.js:
import { createServer, T } from "@mctx-ai/app";
const app = createServer({
instructions: "Use 'get_weather' for current conditions. Provide a city name.",
});
const getWeather = ({ city }) => {
// Your logic here -- call an API, query a database, whatever you need
return `Weather in ${city}: 72F, sunny`;
};
getWeather.description = "Get current weather for a city";
getWeather.input = {
city: T.string({ required: true, description: "City name" }),
};
app.tool("get_weather", getWeather);
export default { fetch: app.fetch };Everything below explains each piece in detail.
package.json fields
Required fields
| Field | What it does | Example |
|---|---|---|
name | Becomes your App's display name | "weather-api" |
version | Tells mctx when you have a new release | "1.0.0" |
description | What subscribers see when discovering your App | "Get real-time weather data for any location" |
main | Path to your built JavaScript file | "dist/index.js" |
Optional fields
| Field | What it does | Example |
|---|---|---|
homepage | A link to your project's homepage shown on your public App page | "https://github.com/your-org/your-server" |
The homepage URL appears as a clickable link on your public mctx.ai App page. Point it at a dedicated project website or the GitHub repository for your App. If you use a GitHub URL, only set this for public repositories — a private repo URL shows a 404 to visitors.
{
"name": "weather-api",
"version": "1.0.0",
"description": "Get real-time weather data for any location worldwide",
"main": "dist/index.js",
"homepage": "https://github.com/your-org/weather-api"
}Version numbers
Use semantic versioning -- three numbers separated by dots:
MAJOR.MINOR.PATCH1.0.0-- your first release1.1.0-- you added a new tool1.1.1-- you fixed a bug2.0.0-- you made breaking changes
Each deployment requires a unique version. Bump the version before pushing to trigger a new deployment.
Pre-release versions work too: 2.0.0-beta.1, 1.3.0-rc.2.
Writing a good description
Your description serves three purposes, and knowing this changes how you write it.
1. MCP Community Registry listing — Only the first 100 characters appear in the registry listing. Developers scanning the registry see this snippet before deciding to click through. Your most compelling hook must live in those first 100 characters.
2. Public App info page — The full description is the first thing visitors see after your App title at mctx.ai/apps/[slug]. It is your primary marketing copy for potential subscribers — the sentence that makes someone decide to keep reading or move on.
3. SEO and Google indexing — The full description, up to 1,000 characters, is used directly for search engine indexing. More specific, relevant content means better Google ranking and more organic discovery. Use as much of the 1,000-character limit as is useful and relevant — this is a direct lever on discoverability.
The first 100 characters are your hook. Lead with that.
| Good | Bad |
|---|---|
| "Query financial data from SEC filings with natural language" | "A server that does things with data" |
| "Search and summarize academic papers from arXiv" | "Useful research tool" |
Start with a verb. Be specific. Then use the remaining characters to expand on what subscribers get, what problems it solves, and what makes it worth subscribing to. A weak description means fewer subscribers — no matter how good your App is.
The main field
This points to your built JavaScript file -- the single .js or .mjs file that mctx loads when running your App. It must be committed to your repository (or built during CI and pushed to a release branch).
Common patterns by build tool:
| Build tool | Typical main value |
|---|---|
| esbuild | dist/index.js |
| tsc (TypeScript) | dist/index.js |
| Rollup | build/index.js |
| No build (plain JS) | dist/index.js (just copy it) |
The fetch handler
Your built file must export a default object with a fetch method. This is the standard Cloudflare Worker format:
export default {
fetch(request, env) {
// Handle incoming requests
},
};If you are using the @mctx-ai/app framework, this is one line:
export default { fetch: app.fetch };The env parameter gives you access to environment variables you set in the mctx dashboard. Useful for API keys, database URLs, and other secrets.
Capabilities are automatic
You might wonder how AI clients know what your App can do. The answer: you do not configure this.
When mctx deploys your App, it calls your App's initialization endpoint and reads the response. If you registered tools, the platform advertises tool support. If you registered resources, it advertises resource support. Same for prompts.
// This App will automatically advertise tools + resources capabilities
app.tool("search", searchHandler);
app.resource("docs://readme", readmeHandler);No config file, no capability declaration. Just register what you have and the platform handles the rest.
Project structure
A typical project looks like this:
my-mcp-server/
src/
index.js # Your source code
dist/
index.js # Built output (committed or CI-built)
package.jsonWorking in a monorepo? Put your package.json in a subdirectory:
my-monorepo/
packages/
my-mcp-server/
src/
index.js
dist/
index.js
package.json
package.jsonWhen creating your App in the mctx dashboard, enter packages/my-mcp-server as the path.
Building your App
Your build output must be a single JavaScript file. This is the same pattern GitHub uses for shared Actions -- bundle your dependencies, commit the output, and it works.
Using esbuild (recommended)
Add this to your package.json:
{
"scripts": {
"build": "esbuild src/index.ts --bundle --minify --platform=node --format=esm --outfile=dist/index.js"
},
"devDependencies": {
"esbuild": "latest"
}
}Important: No --external flags. Everything, including all dependencies, must be bundled into the single output file.
esbuild handles TypeScript natively -- you don't need a separate tsc compilation step. Just point it at your .ts source files and it will transpile and bundle in one pass.
Or create a build.js for more control:
import * as esbuild from "esbuild";
await esbuild.build({
entryPoints: ["src/index.ts"],
bundle: true,
minify: true,
outfile: "dist/index.js",
format: "esm",
platform: "node",
// Do NOT use external -- everything must be bundled
});TypeScript setup
If you are writing TypeScript, add a tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"outDir": "dist",
"strict": true
}
}Then use esbuild for the actual build -- it handles TypeScript natively and produces the single bundled file mctx needs.
Pro tip: the release branch pattern
Committing built files to your main branch clutters pull request diffs with generated code. A cleaner approach: use a release branch.
How it works:
- Your
mainbranch has source code only -- nodist/folder - CI builds the project on merge and pushes the bundled output to a
releasebranch - You configure your App in mctx to deploy from
releaseinstead ofmain
This keeps pull request reviews clean (reviewers see source code, not generated bundles) while keeping deployment automatic.
The example-app template repository includes this GitHub Actions workflow (.github/workflows/release.yml) which copies automatically when you create a new repository from the template.
README.md
Your repository's README.md is displayed on your public App page. When visitors click to view your readme, a modal opens with the sanitized readme content rendered inline — no external link to GitHub required. This means the readme is accessible even for private repositories. No extra configuration needed -- just maintain a README.md in your repository.
Where mctx looks:
- Single-package repos: the file at your repo root (
README.md) - Monorepos: the file inside your server subdirectory (e.g.,
packages/my-app/README.md)
Size limit: The first 50KB of content is displayed. Readmes larger than 50KB are truncated at a safe UTF-8 boundary.
If there is no README.md: Your public page displays without a README section. Adding one later is easy -- just create the file and deploy a new version for it to appear.
See package.json Configuration for more details.
Writing a good README
Your README's audience is potential subscribers and current subscribers — not other developers reading your source code. Write it entirely from the perspective of someone deciding whether to subscribe and, after subscribing, someone figuring out how to get value from your App. Internal implementation notes, contributing guidelines, and developer setup instructions do not belong here.
A note on developer-tool Apps: Some Apps are built for developers — coding assistants, documentation tools, API explorers, workflow helpers. The audience is still subscribers, not implementers. The register stays the same. Write to the developer as a user of your App, not as someone implementing or integrating it. The distinction is subtle but important:
- "Look up the current API spec for any endpoint" — subscriber register (what you can do)
- "The tool exposes endpoints for querying the API spec" — implementer register (how it works)
Even an App whose entire audience consists of developers should read like a product page for that developer, not a technical reference. If the subscriber is a developer, speak to them as someone who wants to get things done — not someone who wants to know how things are wired up.
A weak README means fewer subscribers. A strong README means more subscribers, higher Context7 ranking, and better visibility in AI assistant conversations. The stakes are real — this document directly affects how many people find and use your App.
mctx automatically adds platform metadata (server slug, subscribe link, connection endpoint, version details) when publishing your README to Context7. You do not need to include any mctx branding, platform references, or technical hosting details in your README itself. Focus entirely on user value and usage.
Recommended structure:
1. Opening hook — The first two things a reader sees set the tone for everything that follows. Open with a single punchy one-liner that captures what the App does — the elevator pitch. Follow it immediately with a problem-to-solution paragraph: what problem does the subscriber have without this App, and how does this App solve it? This is not the full value section — it is the hook that makes someone keep reading.
Never lose track of what your AI assistant knows.
Most AI conversations start from scratch. Every session, you re-explain your preferences,
your projects, your context — and the AI forgets again the moment the window closes.
This server gives your AI assistant persistent memory across every session and every client.
Tell it once. It remembers forever.1b. Differentiation statement — Directly after the problem-to-solution paragraph, add one plain statement that answers the obvious subscriber objection for your category of App. For most Apps, that objection is: "Why would I use this instead of just asking my AI directly?" Answer it confidently, not defensively. One sentence is enough. Place it before the benefits list so it lands while the reader is still deciding whether to keep reading.
Unlike asking your AI from scratch, this server gives your assistant access to decisions
and context you've built up over weeks — across every session and every client.The differentiation statement does not need to be exhaustive. It just needs to close the "but why not just..." question before the reader asks it silently and moves on.
2. What you get — Present the server's benefits as a scannable list. Use bold lead phrases (3–6 words) followed by one or two sentences of explanation. Each benefit gets its own item. This is more persuasive and easier to scan than prose paragraphs or plain bullet lists.
### What you get
**Cross-session continuity** — Save a decision today. Open a new session tomorrow and pick
up exactly where you left off.
**Works across every AI client** — Claude, ChatGPT, Cursor, whatever comes next. Your
context travels with you.
**Natural to use** — No commands to learn. Just talk to your AI the way you already do.
The server figures out what to remember.3. How it works (optional) — For Apps with non-obvious mechanics, add a short narrative section that walks through the subscriber experience in plain language. What happens at the start of a session? During a session? Across sessions or machines? This is not a technical description of your implementation — it is a story of what the subscriber experiences. Mark this section optional: only add it when the App's behavior is not self-evident from the benefits.
### How it works
When you start a session with your AI assistant, this server automatically loads your
saved context — notes you have stored, preferences you have set, decisions you have made.
No setup required on your end.
As you work, you can tell your AI to remember things: "Remember that I prefer TypeScript."
"Note that the deadline is March 15." Those instructions are saved immediately and will
be there the next time you start a session, even on a different machine.
Nothing is required from you to use what you have saved. Your AI assistant reads the
context automatically at session start.4. Usage in conversation — How does a subscriber invoke the App's capabilities through their AI client? What does the AI client understand and pick up on? What kinds of natural language trigger the App's functionality?
### Using this server
Once subscribed, your AI assistant understands weather requests naturally. Ask in your
own words — the server handles the lookup.
Your AI client connects to this server automatically when you ask weather-related questions.5. Example phrases grouped by use case — Specific example prompts a subscriber can say in conversation to use the server. Group them under named use case categories — not a flat list. This makes the section navigable and helps subscribers find the phrases that match what they are trying to do right now. Each group should also include 1–2 sentences of context: what situation prompts this group of phrases, and what the subscriber is trying to accomplish. This context makes the section scannable and helps subscribers recognise their own situation at a glance.
### Example phrases
**Saving your work**
You're mid-conversation and want your AI to hold on to something important for later
sessions or on different machines.
- "Remember that I'm working on the Acme redesign project"
- "Save this decision: we're using PostgreSQL for the new service"
- "Note that my preferred coding style is functional, no classes"
**Picking up where you left off**
You're starting a new session and want to continue from where a previous conversation ended,
without re-explaining your context from scratch.
- "What was I working on last time?"
- "Remind me what decisions we made about the API structure"
- "What do you know about my current projects?"
**Managing your knowledge base**
You want to review, update, or remove things you've previously stored.
- "Show me everything you have saved about the Acme project"
- "Forget what I told you about my old email address"
- "Update my preference: I now prefer tabs over spaces"6. Example responses — What the server returns when those phrases are used. This sets subscriber expectations and helps them understand what to ask for.
### Example responses
**"What was I working on last time?"**
> Based on your saved notes, you were working on the Acme redesign project — specifically
> the mobile navigation component. You noted that the client approved the wireframes on
> Monday and wanted the prototype ready by end of week.
**"Remember that my preferred stack is Next.js with TypeScript"**
> Saved. I'll keep in mind that you prefer Next.js with TypeScript for future
> conversations.7. Real-world workflow narrative (strongly recommended) — Add 2–3 short scenarios showing the server being used across time: different days, different machines, different AI clients. This is the most persuasive section a README can have, and its absence is the single most common gap in READMEs that otherwise follow this structure. It helps potential subscribers picture themselves using the server. Show what the user says, what happens, and what they experience as a result. Keep each scenario to 3–5 sentences.
This section is effectively required for any App whose value plays out across sessions, machines, or AI clients — meaning most Apps. The only Apps that can skip it are those whose entire value is delivered in a single, self-contained interaction with no continuity dimension. When in doubt, include it.
What makes a good narrative scenario:
- Specific moment in time — Start with a concrete situation ("She opens a new Claude session Monday morning"), not a generalisation
- What the user says — Include actual natural-language input so the subscriber can picture the conversation
- What they experience — Describe the outcome from the subscriber's point of view, not the App's mechanics
- 2–3 scenarios, not one — Multiple scenarios reach more readers. Each one speaks to a different moment or a different type of subscriber
### How subscribers use it
**Picking up mid-project**
Sarah is three weeks into a feature build. She opens a new Claude session Monday morning
and asks: "What do you know about the payment integration I'm working on?" The server
returns her notes from Friday: the API endpoints she decided on, the edge cases she flagged,
and the teammate she was waiting to hear from. She continues the conversation without
re-explaining anything.
**Consistent preferences everywhere**
Marcus set his preferences once — no semicolons, functional style, prefer explicit error
handling. Now every AI client he opens already knows. He does not re-explain himself in
Cursor, in Claude, or anywhere else.What not to include:
No "Getting Started" or subscription instructions — Do not include a "Getting Started", "How to Subscribe", or "Installation" section. mctx automatically generates and injects this content — the subscribe link, connection instructions, and authentication details — when the README is published to Context7 and when it is displayed on the platform. Including it yourself creates duplication and makes the README feel like generic boilerplate. The platform handles onboarding. Your README's job is to make someone want to subscribe in the first place.
No raw tools list — A list of tool names and technical descriptions does not belong in the README. The MCP protocol delivers tool definitions directly to the AI client — subscribers never see tool names. What serves subscribers is example phrases grouped by use case (covered above), not a technical manifest. Write for the person using the AI, not for the AI itself.
Put boilerplate at the end — Prerequisites, license, contributing guidelines, and installation instructions matter on GitHub. They rarely matter to a potential subscriber. Move them to the bottom, after the content that drives value.
Keep badges and images at the very top or very end — never between descriptive sections.
Context7 and benchmark scoring:
If you have Context7 discovery enabled, your README directly affects your App's Context7 benchmark score. Context7 auto-generates questions developers might ask — things like "what does this App do?", "why would I use it?", and "how do I invoke X?" — and measures how well your documentation answers them. A README structured around the template above naturally answers these questions and will score higher.
Your GitHub repository's activity also factors into Context7's trust score: stars, commit recency, and update frequency all contribute. An actively maintained repo with a clear, current README surfaces more prominently in AI assistant recommendations.
Resource limits
Your App runs as a Cloudflare Worker. Each request gets:
| Resource | Limit |
|---|---|
| CPU time | 5,000ms |
Outbound fetch() calls | 50 per request |
CPU time is a hard limit. Requests exceeding 5,000ms are killed and return a timeout error. Outbound fetch calls are limited to 50 per request—exceeding this will fail.
These limits cover most use cases. If you are calling external APIs, batch requests when possible to stay within the 50-call limit.
Runtime environment
Your App runs on Cloudflare Workers — a V8 isolate environment. This is not Node.js. It does not have a file system, spawnable processes, or network sockets. It has a global edge network and extremely low cold-start latency.
Understanding what is and is not available prevents the most common category of deployment failures.
What the nodejs_compat flag provides
mctx enables the nodejs_compat compatibility flag for every tenant worker. This flag makes a subset of Node.js APIs available in the Workers runtime. Without it, no node:* imports work at all.
Supported modules (safe to use):
| Module | Notes |
|---|---|
node:crypto | Full cryptographic API |
node:buffer | Buffer and encoding utilities |
node:stream | Readable, Writable, Transform streams |
node:util | Formatting, promisify, inspect |
node:events | EventEmitter |
node:assert | Assertion functions |
node:url | URL parsing and construction |
node:path | Path manipulation utilities |
Unsupported modules (these do not exist in the Workers runtime):
| Module | Why it is not available |
|---|---|
node:net | Raw TCP sockets require OS networking, which Workers do not have |
node:dgram | UDP sockets — same reason as node:net |
node:cluster | Process clustering is meaningless in an isolate model |
node:worker_threads | Workers use a different concurrency model entirely |
Limited or experimental modules:
| Module | Status |
|---|---|
node:fs | Experimental. Available with a specific compatibility date — not currently enabled by mctx for tenant workers |
node:child_process | Experimental. Requires additional flags not enabled by mctx |
If your App imports any unsupported module — directly or through a dependency — deployment fails with validation error 10021.
Common pitfalls
The trickier failures come from transitive dependencies: packages that look harmless but pull in unsupported APIs internally.
Example: using node:child_process directly
import { execSync } from "node:child_process"; // Fails — not availableExample: a library that wraps child_process
import shelljs from "shelljs"; // Fails — shelljs calls execSync internally
import execa from "execa"; // Fails — execa uses child_process under the hoodExample: a networking library
import net from "node:net"; // Fails — TCP sockets not available
import { createConnection } from "node:net"; // Same — failsHow to check your dependencies
Run this in your project to find modules importing unsupported APIs:
npx esbuild src/index.ts --bundle --platform=browser --format=esm --outfile=/dev/null 2>&1 | grep "Cannot use"Use --platform=browser (not --platform=node) to surface the same incompatibilities the Workers runtime enforces.
Alternatives that work in Workers
| What you want | What to use instead |
|---|---|
| Run a shell command | Not possible in Workers. Move that logic to an external API your App can call with fetch() |
| Read a file | Bundle the content into your built file at build time, or serve it from an external URL |
| TCP connections | Use a vendor SDK that supports HTTP-based connections (most modern databases do) |
| Spawn a subprocess | Not possible. Consider a dedicated worker service accessible via HTTP |
Checking compatibility before you deploy
The safest approach is to build and validate locally before pushing:
# Build with the Workers-compatible target
npx esbuild src/index.ts --bundle --platform=browser --format=esm --outfile=dist/index.js
# If it builds without errors, it will deployThis is a diagnostic technique only. Do not commit browser-targeted bundles or use
--platform=browser in your actual build configuration. Your real build should use
--platform=node (as shown in the esbuild setup above). The browser
target is used here solely to surface the same incompatibilities the Workers runtime enforces — it
is not a deployable output.
If esbuild exits with errors about unsupported Node.js APIs, fix them before pushing. They will fail at deployment just the same.
Validation errors
mctx validates your package.json when you select a repository and when you push new versions. Here are the most common errors and how to fix them:
| Error | Fix |
|---|---|
| "package.json not found" | Add the file to your repository root (or the subdirectory path you specified) |
| "Invalid version format" | Use semver format: 1.0.0, not v1 or 1.0 |
| "Main file not found" | Run your build and commit the output file, or set up CI to build it |
| "Description too long" | Keep it under 1000 characters |
Next Steps
- Tools, Resources, and Prompts -- the three building blocks for everything your App can do
- Deploy Your App -- push to GitHub and go live
See something wrong? Report it or suggest an improvement — your feedback helps make these docs better.
Scheduled Event Delivery
Emit events that the platform holds and delivers at a specific time. Use deliverAt to defer events, key to supersede stale ones, and ctx.cancel() to remove pending events before they arrive.
Framework API Reference
Complete reference for @mctx-ai/app — all exports, types, and patterns.