
TL;DR
A step-by-step guide to building Model Context Protocol servers in TypeScript. Project setup, tool registration, resources, testing with Claude Code, and production patterns.
You have used MCP servers. You have configured them for Claude Code and Cursor. Now it is time to build your own.
The Model Context Protocol lets AI agents connect to external tools and data through a standard interface. There are thousands of community-built servers, but sometimes you need something specific to your workflow. A server that talks to your internal API. One that queries your production database. A tool that wraps your company's deployment pipeline.
This guide walks you through building an MCP server from scratch in TypeScript. By the end, you will have a working server with tools, resources, and prompts that you can connect to Claude Code, Claude Desktop, or any MCP-compatible client.
Quick refresher. MCP uses a client-server architecture. Your AI tool (Claude Code, Cursor, Claude Desktop) is the client. It connects to one or more MCP servers, each exposing capabilities through three primitives:
The client discovers what your server offers through a handshake, then the AI model decides which tools to call based on the user's request. Communication happens over stdio (local processes) or HTTP (remote servers).
For the full protocol deep dive, read What is MCP. Here we are building.
You need:
node --version to check)No prior MCP experience required.
Create a new directory and initialize the project:
mkdir my-mcp-server
cd my-mcp-server
npm init -y
Install the MCP SDK and dependencies:
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/nodeThe @modelcontextprotocol/sdk package is the official TypeScript SDK for building MCP servers and clients. zod handles input validation. The SDK uses Zod schemas to define tool parameters, so you get automatic type checking and clear error messages out of the box.
Initialize TypeScript:
npx tsc --init
Replace the generated tsconfig.json with these settings:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"]
}
The module and moduleResolution must be Node16 (or NodeNext). The MCP SDK uses ES module exports with subpath imports, and this config makes TypeScript resolve them correctly.
Update package.json to add the module type and scripts:
{
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
}
}
Create the source directory:
mkdir src
Your project structure:
my-mcp-server/
src/
package.json
tsconfig.json
node_modules/
Create src/index.ts with the simplest possible MCP server:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// Create the server
const server = new McpServer({
name: "my-first-server",
version: "1.0.0",
});
// Add a simple tool
server.tool(
"greet",
"Generate a greeting for someone",
{ name: z.string().describe("The person's name") },
async ({ name }) => ({
content: [{ type: "text", text: `Hello, ${name}! Welcome to MCP.` }],
})
);
// Connect via stdio and start listening
const transport = new StdioServerTransport();
await server.connect(transport);
Four things happening here:
McpServer is the main class. The name and version identify your server to clients.server.tool() registers a tool. It takes the tool name, a description (the AI reads this to decide when to use it), a Zod schema for input validation, and an async handler.StdioServerTransport means the server communicates over stdin/stdout. This is the transport used by Claude Code, Claude Desktop, and Cursor.server.connect(transport) starts listening for JSON-RPC messages.Build and verify:
npx tsc
No errors? You have a working MCP server. It just does not do much yet.
A useful server exposes multiple tools. Here is a more practical example. A server that manages bookmarks:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { readFileSync, writeFileSync, existsSync } from "node:fs";
import { join } from "node:path";
import { randomUUID } from "node:crypto";
// --- Types ---
interface Bookmark {
id: string;
url: string;
title: string;
tags: string[];
createdAt: string;
}
// --- Data Layer ---
const DATA_FILE = join(process.cwd(), "bookmarks.json");
function loadBookmarks(): Bookmark[] {
if (!existsSync(DATA_FILE)) return [];
try {
return JSON.parse(readFileSync(DATA_FILE, "utf-8")) as Bookmark[];
} catch {
return [];
}
}
function saveBookmarks(bookmarks: Bookmark[]): void {
writeFileSync(DATA_FILE, JSON.stringify(bookmarks, null, 2), "utf-8");
}
// --- Server ---
const server = new McpServer({
name: "bookmarks-server",
version: "1.0.0",
});
// Tool: Add a bookmark
server.tool(
"add_bookmark",
"Save a new bookmark with a URL, title, and optional tags",
{
url: z.string().url().describe("The URL to bookmark"),
title: z.string().describe("A short title for the bookmark"),
tags: z
.array(z.string())
.optional()
.describe("Optional tags for categorization, e.g. ['dev', 'reference']"),
},
async ({ url, title, tags }) => {
const bookmarks = loadBookmarks();
const bookmark: Bookmark = {
id: randomUUID(),
url,
title,
tags: tags ?? [],
createdAt: new Date().toISOString(),
};
bookmarks.push(bookmark);
saveBookmarks(bookmarks);
return {
content: [
{
type: "text",
text: `Bookmark saved.\n\nID: ${bookmark.id}\nTitle: ${bookmark.title}\nURL: ${bookmark.url}\nTags: ${bookmark.tags.join(", ") || "none"}`,
},
],
};
}
);
// Tool: Search bookmarks
server.tool(
"search_bookmarks",
"Search bookmarks by keyword in title or URL",
{
query: z.string().describe("Search keyword or phrase"),
},
async ({ query }) => {
const bookmarks = loadBookmarks();
const lower = query.toLowerCase();
const matches = bookmarks.filter(
(b) =>
b.title.toLowerCase().includes(lower) ||
b.url.toLowerCase().includes(lower) ||
b.tags.some((t) => t.toLowerCase().includes(lower))
);
if (matches.length === 0) {
return {
content: [{ type: "text", text: `No bookmarks match "${query}".` }],
};
}
const results = matches
.map((b) => `- **${b.title}**\n ${b.url}\n Tags: ${b.tags.join(", ") || "none"}`)
.join("\n\n");
return {
content: [
{
type: "text",
text: `Found ${matches.length} bookmark(s):\n\n${results}`,
},
],
};
}
);
// Tool: List all bookmarks
server.tool(
"list_bookmarks",
"List all saved bookmarks, optionally filtered by tag",
{
tag: z
.string()
.optional()
.describe("Filter by tag. Omit to return all bookmarks."),
},
async ({ tag }) => {
let bookmarks = loadBookmarks();
if (tag) {
bookmarks = bookmarks.filter((b) =>
b.tags.some((t) => t.toLowerCase() === tag.toLowerCase())
);
}
if (bookmarks.length === 0) {
return {
content: [
{
type: "text",
text: tag
? `No bookmarks with tag "${tag}".`
: "No bookmarks yet. Use add_bookmark to save one.",
},
],
};
}
const list = bookmarks
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
.map((b) => `- **${b.title}** - ${b.url} [${b.tags.join(", ")}]`)
.join("\n");
return {
content: [
{ type: "text", text: `${bookmarks.length} bookmark(s):\n\n${list}` },
],
};
}
);
// Tool: Delete a bookmark
server.tool(
"delete_bookmark",
"Delete a bookmark by its ID",
{
id: z.string().describe("The UUID of the bookmark to delete"),
},
async ({ id }) => {
const bookmarks = loadBookmarks();
const index = bookmarks.findIndex((b) => b.id === id);
if (index === -1) {
return {
content: [{ type: "text", text: `Bookmark "${id}" not found.` }],
isError: true,
};
}
const deleted = bookmarks.splice(index, 1)[0];
saveBookmarks(bookmarks);
return {
content: [
{
type: "text",
text: `Deleted: "${deleted.title}" (${deleted.url})`,
},
],
};
}
);
Key patterns to notice:
z.string().url() validates that the input is actually a URL. The AI sees these constraints and provides valid input.isError: true. This tells the AI the operation did not succeed so it can report the failure or retry..describe() on every field. The AI reads these descriptions to decide what values to pass. Be specific. "The URL to bookmark" is better than "URL".add_bookmark, search_bookmarks, list_bookmarks, delete_bookmark. Not a single manage_bookmarks tool with a mode flag.Get the weekly deep dive
Tutorials on Claude Code, AI agents, and dev tools - delivered free every week.
Resources expose read-only data to the AI. Unlike tools (which perform actions), resources provide context. Config files, documentation, status information.
Add these below your tools:
// Resource: All bookmarks as a readable document
server.resource(
"all-bookmarks",
"bookmarks://all",
async (uri) => {
const bookmarks = loadBookmarks();
const document =
bookmarks.length === 0
? "No bookmarks saved yet."
: bookmarks
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
.map(
(b) =>
`## ${b.title}\n- URL: ${b.url}\n- Tags: ${b.tags.join(", ") || "none"}\n- Added: ${b.createdAt}`
)
.join("\n\n");
return {
contents: [
{
uri: uri.href,
mimeType: "text/markdown",
text: document,
},
],
};
}
);
// Resource: Server stats
server.resource(
"stats",
"bookmarks://stats",
async (uri) => {
const bookmarks = loadBookmarks();
const allTags = bookmarks.flatMap((b) => b.tags);
const uniqueTags = [...new Set(allTags)];
const stats = {
totalBookmarks: bookmarks.length,
totalTags: uniqueTags.length,
topTags: uniqueTags
.map((tag) => ({
tag,
count: allTags.filter((t) => t === tag).length,
}))
.sort((a, b) => b.count - a.count)
.slice(0, 5),
};
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(stats, null, 2),
},
],
};
}
);
The first argument is a display name. The second is a URI that clients use to request the resource. The handler returns the data.
You can also create resource templates for dynamic data using URI template parameters:
// Dynamic resource - look up bookmarks by tag
server.resource(
"bookmarks-by-tag",
"bookmarks://tags/{tag}",
async (uri, { tag }) => {
const bookmarks = loadBookmarks().filter((b) =>
b.tags.some((t) => t.toLowerCase() === (tag as string).toLowerCase())
);
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(bookmarks, null, 2),
},
],
};
}
);
Prompts are reusable templates that guide the AI's behavior for specific workflows. Unlike tools (called by the AI model) and resources (read by the AI), prompts are typically selected by the user to start a structured interaction.
// Prompt: Organize bookmarks
server.prompt(
"organize_bookmarks",
"Analyze all bookmarks and suggest a better tagging system",
{},
() => {
const bookmarks = loadBookmarks();
const bookmarkList =
bookmarks.length === 0
? "No bookmarks saved."
: bookmarks
.map((b) => `- ${b.title} (${b.url}) [tags: ${b.tags.join(", ") || "none"}]`)
.join("\n");
return {
messages: [
{
role: "user" as const,
content: {
type: "text" as const,
text: [
"Here are all my bookmarks:",
"",
bookmarkList,
"",
"Please:",
"1. Identify common themes",
"2. Suggest a consistent tagging taxonomy",
"3. Flag any duplicates or dead-looking URLs",
"4. Recommend which bookmarks to re-tag using the new taxonomy",
].join("\n"),
},
},
],
};
}
);
Add the transport connection at the bottom of your file:
// Start the server
const transport = new StdioServerTransport();
await server.connect(transport);
Build the project:
npx tsc
Your compiled server lives at dist/index.js. Time to connect it to something.
Claude Code reads MCP configuration from .claude/settings.json in your project directory (or ~/.claude/settings.json for global servers).
Add your server:
{
"mcpServers": {
"bookmarks": {
"command": "node",
"args": ["/absolute/path/to/my-mcp-server/dist/index.js"]
}
}
}
Replace /absolute/path/to/my-mcp-server/dist/index.js with the actual path to your compiled file.
Restart Claude Code. It will spawn your server process, perform the MCP handshake, and discover your tools. Now you can use them in conversation:
Claude Code calls the right tool automatically based on your request.
The process is similar. Open your Claude Desktop config:
~/Library/Application Support/Claude/claude_desktop_config.json%APPDATA%\Claude\claude_desktop_config.json~/.config/Claude/claude_desktop_config.jsonAdd the same server entry:
{
"mcpServers": {
"bookmarks": {
"command": "node",
"args": ["/absolute/path/to/my-mcp-server/dist/index.js"]
}
}
}
Restart Claude Desktop. You will see a hammer icon in the chat input. Click it to see your tools.
You do not need Claude to test your server. The MCP project provides an official testing tool:
npx @modelcontextprotocol/inspector node dist/index.js
This opens a web UI (usually at http://localhost:5173) where you can:
Use the Inspector during development to verify schemas, test edge cases, and debug issues before connecting to Claude.
Things not working? Check these:
Logs. Claude Desktop writes MCP logs to ~/Library/Logs/Claude/mcp*.log (macOS) or %APPDATA%\Claude\logs\mcp*.log (Windows). Claude Code logs appear in its terminal output.
Absolute paths. The args path in your config must be absolute and point to the compiled .js file, not the .ts source.
Module type. Make sure "type": "module" is in your package.json. Without it, Node.js cannot import the MCP SDK's ES modules.
Manual test. Pipe a JSON-RPC initialize message directly to your server:
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0.0"}}}' | node dist/index.js
If the server works, you will see a JSON-RPC response with its capabilities.
Once you have the basics working, here are patterns that make your server production-ready.
Always return isError: true on failure. Wrap handlers in try/catch:
server.tool("risky_operation", "Do something that might fail", {
input: z.string(),
}, async ({ input }) => {
try {
const result = await doSomething(input);
return { content: [{ type: "text", text: result }] };
} catch (error) {
return {
content: [{ type: "text", text: `Error: ${(error as Error).message}` }],
isError: true,
};
}
});
The AI reads your descriptions to decide when and how to use tools. Be specific:
// Vague - the AI has to guess
{ date: z.string().describe("Date") }
// Specific - the AI knows exactly what to provide
{ date: z.string().describe("Date in YYYY-MM-DD format, e.g. 2026-04-02") }
Same goes for tool descriptions. "Query the database" is worse than "Run a read-only SQL query against the production PostgreSQL database. Returns up to 100 rows."
stdio is great for local development. For production or team deployments, use Streamable HTTP transport:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
const app = express();
app.use(express.json());
const server = new McpServer({
name: "remote-bookmarks",
version: "1.0.0",
});
// ... register tools, resources, prompts ...
app.post("/mcp", async (req, res) => {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
res.on("close", () => transport.close());
await server.connect(transport);
await transport.handleRequest(req, res);
});
app.listen(3001, () => {
console.log("MCP server running at http://localhost:3001/mcp");
});
Clients connect to your server over HTTP instead of spawning a local process. This is how you deploy MCP servers for teams or as public services.
One tool, one job. This makes it easier for the AI to pick the right tool and reduces the chance of invalid input combinations.
Instead of:
server.tool("manage_bookmarks", "Manage bookmarks", {
action: z.enum(["add", "delete", "search", "list"]),
// ... conditional params
});
Use separate tools:
server.tool("add_bookmark", "Save a new bookmark", { /* ... */ });
server.tool("delete_bookmark", "Delete a bookmark by ID", { /* ... */ });
server.tool("search_bookmarks", "Search bookmarks by keyword", { /* ... */ });
server.tool("list_bookmarks", "List all bookmarks", { /* ... */ });
Here is the full src/index.ts with everything wired together. Copy this, build it, and you have a working MCP bookmarks server:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { readFileSync, writeFileSync, existsSync } from "node:fs";
import { join } from "node:path";
import { randomUUID } from "node:crypto";
interface Bookmark {
id: string;
url: string;
title: string;
tags: string[];
createdAt: string;
}
const DATA_FILE = join(process.cwd(), "bookmarks.json");
function loadBookmarks(): Bookmark[] {
if (!existsSync(DATA_FILE)) return [];
try {
return JSON.parse(readFileSync(DATA_FILE, "utf-8")) as Bookmark[];
} catch {
return [];
}
}
function saveBookmarks(bookmarks: Bookmark[]): void {
writeFileSync(DATA_FILE, JSON.stringify(bookmarks, null, 2), "utf-8");
}
const server = new McpServer({
name: "bookmarks-server",
version: "1.0.0",
});
server.tool(
"add_bookmark",
"Save a new bookmark with a URL, title, and optional tags",
{
url: z.string().url().describe("The URL to bookmark"),
title: z.string().describe("A short title for the bookmark"),
tags: z.array(z.string()).optional().describe("Optional tags, e.g. ['dev', 'reference']"),
},
async ({ url, title, tags }) => {
const bookmarks = loadBookmarks();
const bookmark: Bookmark = {
id: randomUUID(),
url,
title,
tags: tags ?? [],
createdAt: new Date().toISOString(),
};
bookmarks.push(bookmark);
saveBookmarks(bookmarks);
return {
content: [{
type: "text",
text: `Saved: ${bookmark.title} (${bookmark.url}) [${bookmark.tags.join(", ") || "none"}]`,
}],
};
}
);
server.tool(
"search_bookmarks",
"Search bookmarks by keyword in title, URL, or tags",
{ query: z.string().describe("Search keyword or phrase") },
async ({ query }) => {
const lower = query.toLowerCase();
const matches = loadBookmarks().filter(
(b) =>
b.title.toLowerCase().includes(lower) ||
b.url.toLowerCase().includes(lower) ||
b.tags.some((t) => t.toLowerCase().includes(lower))
);
if (matches.length === 0) {
return { content: [{ type: "text", text: `No bookmarks match "${query}".` }] };
}
const results = matches
.map((b) => `- **${b.title}**\n ${b.url}\n Tags: ${b.tags.join(", ") || "none"}`)
.join("\n\n");
return { content: [{ type: "text", text: `Found ${matches.length}:\n\n${results}` }] };
}
);
server.tool(
"list_bookmarks",
"List all bookmarks, optionally filtered by tag",
{ tag: z.string().optional().describe("Filter by tag. Omit for all.") },
async ({ tag }) => {
let bookmarks = loadBookmarks();
if (tag) {
bookmarks = bookmarks.filter((b) =>
b.tags.some((t) => t.toLowerCase() === tag.toLowerCase())
);
}
if (bookmarks.length === 0) {
return {
content: [{
type: "text",
text: tag ? `No bookmarks tagged "${tag}".` : "No bookmarks yet.",
}],
};
}
const list = bookmarks
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
.map((b) => `- ${b.title} - ${b.url} [${b.tags.join(", ")}]`)
.join("\n");
return { content: [{ type: "text", text: `${bookmarks.length} bookmark(s):\n\n${list}` }] };
}
);
server.tool(
"delete_bookmark",
"Delete a bookmark by its ID",
{ id: z.string().describe("The UUID of the bookmark to delete") },
async ({ id }) => {
const bookmarks = loadBookmarks();
const index = bookmarks.findIndex((b) => b.id === id);
if (index === -1) {
return {
content: [{ type: "text", text: `Bookmark "${id}" not found.` }],
isError: true,
};
}
const deleted = bookmarks.splice(index, 1)[0];
saveBookmarks(bookmarks);
return {
content: [{ type: "text", text: `Deleted: "${deleted.title}" (${deleted.url})` }],
};
}
);
server.resource("all-bookmarks", "bookmarks://all", async (uri) => {
const bookmarks = loadBookmarks();
const doc = bookmarks.length === 0
? "No bookmarks."
: bookmarks
.map((b) => `## ${b.title}\n${b.url}\nTags: ${b.tags.join(", ") || "none"}`)
.join("\n\n");
return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: doc }] };
});
server.resource("stats", "bookmarks://stats", async (uri) => {
const bookmarks = loadBookmarks();
const allTags = bookmarks.flatMap((b) => b.tags);
const uniqueTags = [...new Set(allTags)];
return {
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify({
total: bookmarks.length,
tags: uniqueTags.length,
topTags: uniqueTags
.map((tag) => ({ tag, count: allTags.filter((t) => t === tag).length }))
.sort((a, b) => b.count - a.count)
.slice(0, 5),
}, null, 2),
}],
};
});
server.prompt(
"organize_bookmarks",
"Analyze bookmarks and suggest a better tagging system",
{},
() => {
const bookmarks = loadBookmarks();
const list = bookmarks
.map((b) => `- ${b.title} (${b.url}) [${b.tags.join(", ") || "none"}]`)
.join("\n");
return {
messages: [{
role: "user" as const,
content: {
type: "text" as const,
text: `Here are my bookmarks:\n\n${list || "None yet."}\n\nPlease suggest a consistent tagging taxonomy, flag duplicates, and recommend re-tags.`,
},
}],
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
Build and run:
npx tsc
npx @modelcontextprotocol/inspector node dist/index.js
Function calling is a feature of individual AI models. You define functions in the API request, and the model can choose to call them. MCP is a protocol layer above that. It standardizes how servers expose tools so any MCP-compatible client can discover and use them. You build the server once. Every client (Claude Code, Cursor, Claude Desktop, VS Code Copilot) can use it.
No. MCP has official SDKs for TypeScript, Python, Java, Kotlin, and C#. There are also community SDKs for Rust, Go, Ruby, and others. This guide uses TypeScript because most web developers are already comfortable with it.
@modelcontextprotocol/server package?Yes. The SDK is being consolidated into a single @modelcontextprotocol/server package with a flatter import structure. If you are starting fresh and want the latest API, install @modelcontextprotocol/server instead and use import { McpServer, StdioServerTransport } from '@modelcontextprotocol/server'. The server.tool() API becomes server.registerTool() with a slightly different signature. Both packages work. This tutorial uses @modelcontextprotocol/sdk because it is the most widely documented and deployed version as of April 2026.
Package it as an npm module. Add a bin field to package.json pointing to your compiled entry file. Users install it globally (npm install -g your-mcp-server) and configure it in their MCP client. For discoverability, list it in the MCP Servers Directory or community registries.
For HTTP-transport servers that need auth, add standard authentication middleware (API keys, OAuth, JWT) to your Express/Fastify server before the MCP handler. The MCP protocol itself does not define auth. It is up to your HTTP layer.
For tools that take more than a few seconds, use progress reporting. The MCP SDK supports progress tokens that let clients show progress indicators. Return partial results when possible rather than blocking for minutes.
You can, but be careful. Always use read-only connections for resource access. For write operations through tools, add validation, rate limiting, and audit logging. Never expose raw SQL execution to the AI. Instead, create specific tools like run_saved_query or insert_record with constrained inputs.
Now that you know the pattern, here are practical servers worth building:
The best MCP servers solve a specific problem you hit daily. Start there.
For more on using existing servers, read How to Use MCP Servers. For the protocol fundamentals, start with What is MCP.
Technical content at the intersection of AI and development. Building with AI agents, Claude Code, and modern dev tools - then showing you exactly how it works.
The TypeScript toolkit for building AI apps. Unified API across OpenAI, Anthropic, Google. Streaming, tool calling, stru...
View ToolTypeScript-first AI agent framework. Workflows, RAG, tool use, evals, and integrations. Built for production Node.js app...
View ToolNew tutorials, open-source projects, and deep dives on coding agents - delivered weekly.
LLM data framework for connecting custom data sources to language models. Best-in-class RAG, data connectors, and query...
Configure Claude Code for maximum productivity -- CLAUDE.md, sub-agents, MCP servers, and autonomous workflows.
AI AgentsWhat MCP servers are, how they work, and how to build your own in 5 minutes.
AI AgentsInstall Claude Code, configure your first project, and start shipping code with AI in under 5 minutes.
Getting Started
Getting Started with OpenAI's New TypeScript Agents SDK: A Comprehensive Guide OpenAI has recently unveiled their Agents SDK within TypeScript, and this video provides a detailed walkthrough...

Check out Tempo: https://tempo.new/?utm_source=youtube&utm_campaign=developer_ai_mcp In this video, I showcase Tempo, a powerful platform that simplifies the process of creating React applications...

Sign up for a free Neon account today and get 10 complimentary projects at https://fyi.neon.tech/1dd! Building a Full Stack AI Enabled Platform: Step-by-Step Tutorial In this video, I'll...
Everything you need to know about Model Context Protocol - what it is, how it works, how to install servers, how to buil...
MCP servers connect AI agents to databases, APIs, and tools through a standard protocol. Here is how to configure and us...

A practical guide to using Claude Code in Next.js projects. CLAUDE.md config for App Router, common workflows, sub-agent...