AI Agents
MCP resources expose readable content to AI agents; MCP prompts inject reusable instructions.
The Model Context Protocol defines three primitives: tools, resources, and prompts. Tools get all the attention because they are the most versatile - an agent calls a tool, the tool does something, the result comes back. But tools are poorly suited to two common needs: exposing large, read-only content that an agent should be able to browse (a code file, a database schema, an API spec) and injecting reusable instructions that appear in a specific part of the conversation without occupying a tool call slot. Resources and prompts fill exactly these gaps, and understanding when to reach for each over a tool produces cleaner, more efficient agent integrations.
A resource is anything with a URI and readable content: a file, a database row, an API endpoint's documentation, a configuration value. The agent client (Claude Code or a custom orchestrator) reads the resource and uses its content as context. Resources are appropriate when:
Resources are a poor fit for: content that requires computation to generate, content that changes per-request based on parameters, or content that an agent should write to (use a tool for that).
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
const server = new McpServer({
name: 'project-context',
version: '1.0.0',
});
// Static resource - database schema documentation
server.resource(
'db-schema',
'postgres://internal/schema',
{
name: 'Database Schema',
description: 'Complete PostgreSQL schema including all tables, columns, constraints, and relationships.',
mimeType: 'text/plain',
},
async (uri) => {
const schema = readFileSync('./docs/schema.sql', 'utf8');
return {
contents: [{
uri: uri.toString(),
mimeType: 'text/plain',
text: schema,
}],
};
}
);
// Dynamic resource - live API spec fetched at read time
server.resource(
'api-spec',
new URL('openapi://internal/spec'),
{
name: 'OpenAPI Specification',
description: 'Current OpenAPI 3.0 spec for the internal REST API.',
mimeType: 'application/json',
},
async (uri) => {
const spec = await fetchCurrentApiSpec();
return {
contents: [{
uri: uri.toString(),
mimeType: 'application/json',
text: JSON.stringify(spec, null, 2),
}],
};
}
);
// Resource template - parameterised: reads any table by name
server.resource(
'table-data',
new URL('postgres://internal/tables/{name}'),
{
name: 'Table Contents',
description: 'Read the first 100 rows of any named table.',
mimeType: 'application/json',
},
async (uri) => {
const tableName = uri.pathname.split('/').pop();
if (!tableName) throw new Error('Table name required');
const rows = await queryTable(tableName, 100);
return {
contents: [{
uri: uri.toString(),
mimeType: 'application/json',
text: JSON.stringify(rows, null, 2),
}],
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
An MCP prompt is a named, parameterised instruction template that a client can invoke to get a pre-constructed set of messages. The client presents available prompts to the user (in Claude Code, these appear as slash commands), the user selects one with optional parameters, and the resulting messages are injected into the conversation. Prompts are appropriate when you have a repeatable task pattern that always starts with the same context and instructions, the instructions depend on parameters that vary per invocation, or you want to standardise how agents approach a domain-specific task.
// MCP prompt for a standardised code review workflow
server.prompt(
'code-review',
'Perform a structured code review on the specified files.',
{
files: {
type: 'string',
description: 'Comma-separated list of file paths to review.',
required: true,
},
focus: {
type: 'string',
description: 'Review focus: "security" | "performance" | "correctness" | "all"',
required: false,
},
},
async ({ files, focus = 'all' }) => {
const fileList = files.split(',').map(f => f.trim());
const fileContents = await Promise.all(
fileList.map(async (path) => ({
path,
content: readFileSync(path'utf8'),
}))
);
const fileSection = fileContents
.map(f => '--- ' + f.path + ' ---
' + f.content)
.join('
');
const checklist = [
focus === 'security' || focus === 'all'
? '- SQL injection, XSS, auth bypass, secret exposure'
: '',
focus === 'performance' || focus === 'all'
? '- N+1 queries, unnecessary re-renders, memory leaks'
: '',
focus === 'correctness' || focus === 'all'
? '- Off-by-one errors, null handling, edge cases'
: '',
].filter(Boolean).join('
');
const text = [
'Perform a ' + focus + ' code review on the following files.',
'',
'Files to review:',
fileSection,
'',
'Review checklist for ' + focus + ':',
checklist,
'',
'For each issue: state file:line, severity (Critical/High/Medium/Low), description, and fix.',
].join('
');
return {
messages: [{
role: 'user' as const,
content: { type: 'text' as const, text },
}],
};
}
);
// Simpler prompt - parameter injection without file access
server.prompt(
'explain-error',
'Get a structured explanation of an error message with fix suggestions.',
{
error: {
type: 'string',
description: 'The full error message or stack trace.',
required: true,
},
language: {
type: 'string',
description: 'Programming language context, e.g. "TypeScript" or "Python".',
required: false,
},
},
async ({ error, language = 'TypeScript' }) => {
const text = [
'Explain this ' + language + ' error and provide a fix:',
'',
error,
'',
'Format your response as:',
'1. Root cause (one sentence)',
'2. Why it happens (2-3 sentences)',
'3. Fix (with code example if applicable)',
'4. Prevention (how to avoid this class of error)',
].join('
');
return {
messages: [{
role: 'user' as const,
content: { type: 'text' as const, text },
}],
};
}
);
Use this to decide which primitive fits a given need:
For the tools layer that complements resources and prompts, see advanced tool use patterns. For writing the agent definitions in Claude Code that invoke these MCP primitives, see the Claude Code subagents guide.