Construire un serveur MCP de A à Z : tools, resources, prompts, validation, transports stdio et HTTP — pour connecter Claude à n'importe quel système externe.
Les resources sont du contenu que Claude peut lire de façon proactive — sans que vous ayez déclenché un tool. Pensez à elles comme des "fichiers virtuels" exposés par votre serveur : logs d'application, configuration système, état d'une base de données, documentation générée dynamiquement.
Tool → action, effet de bord, résultat dynamique selon les paramètres
Resource → contenu, lecture, données à un instant TTool : create_user(name, email) → crée l'utilisateur, retourne l'ID
Resource : users://list → lit la liste des utilisateursLes resources sont identifiées par une URI. Claude les lit avec read_resource(uri).
const server = new Server(
{ name: 'mon-mcp', version: '1.0.0' },
{
capabilities: {
tools: {},
resources: {}, // ← activer les resources
},
}
)import {
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js'
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{
uri: 'app://config',
name: 'Configuration de l\'application',
description: 'Variables d\'environnement et configuration actuelle',
mimeType: 'application/json',
},
{
uri: 'app://logs/recent',
name: 'Logs récents',
description: 'Les 100 dernières entrées de log',
mimeType: 'text/plain',
},
{
uri: 'db://stats',
name: 'Statistiques base de données',
description: 'Nombre d\'entrées par table, taille, dernière mise à jour',
mimeType: 'application/json',
},
],
}))server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params
if (uri === 'app://config') {
const config = {
nodeEnv: process.env.NODE_ENV,
port: process.env.PORT,
dbHost: process.env.DB_HOST,
// Ne jamais exposer les secrets complets
dbPasswordSet: !!process.env.DB_PASSWORD,
version: process.env.npm_package_version,
}
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(config, null, 2),
},
],
}
}
if (uri === 'app://logs/recent') {
const logs = await readRecentLogs(100) // votre fonction de lecture
return {
contents: [
{
uri,
mimeType: 'text/plain',
text: logs.join('\n'),
},
],
}
}
if (uri === 'db://stats') {
const stats = await getDatabaseStats()
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(stats, null, 2),
},
],
}
}
throw new McpError(ErrorCode.InvalidRequest, `Resource inconnue : ${uri}`)
})Une URI template expose des patterns plutôt que des URIs fixes. Claude peut construire l'URI selon ses besoins.
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{
uri: 'users://list',
name: 'Liste des utilisateurs',
mimeType: 'application/json',
},
],
// URI templates — patterns avec variables
resourceTemplates: [
{
uriTemplate: 'users://{id}',
name: 'Profil utilisateur',
description: 'Profil complet d\'un utilisateur par son ID',
mimeType: 'application/json',
},
{
uriTemplate: 'logs://{service}/{date}',
name: 'Logs d\'un service à une date',
description: 'Logs filtrés par service et date (YYYY-MM-DD)',
mimeType: 'text/plain',
},
],
}))Le handler gère les URIs dynamiques avec un parser :
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params
// URI statique
if (uri === 'users://list') {
const users = await db.query('SELECT id, name, email FROM users LIMIT 100')
return {
contents: [{
uri,
mimeType: 'application/json',
text: JSON.stringify(users.rows, null, 2),
}],
}
}
// URI dynamique : users://42
const userMatch = uri.match(/^users:\/\/(\d+)$/)
if (userMatch) {
const id = parseInt(userMatch[1])
const user = await db.query('SELECT * FROM users WHERE id = $1', [id])
if (user.rows.length === 0) {
throw new McpError(ErrorCode.InvalidRequest, `Utilisateur ${id} introuvable`)
}
return {
contents: [{
uri,
mimeType: 'application/json',
text: JSON.stringify(user.rows[0], null, 2),
}],
}
}
// URI dynamique : logs://api/2026-06-24
const logsMatch = uri.match(/^logs:\/\/([a-z-]+)\/(\d{4}-\d{2}-\d{2})$/)
if (logsMatch) {
const [, service, date] = logsMatch
const logs = await fetchLogs(service, date)
return {
contents: [{
uri,
mimeType: 'text/plain',
text: logs,
}],
}
}
throw new McpError(ErrorCode.InvalidRequest, `URI non supportée : ${uri}`)
})Claude comprend le template et peut construire ses propres URIs :
> lis les logs du service "api" pour hierClaude construit logs://api/2026-06-23 et lit la resource.
Les clients peuvent s'abonner à une resource et recevoir des notifications quand son contenu change.
const server = new Server(
{ name: 'mon-mcp', version: '1.0.0' },
{
capabilities: {
resources: {
subscribe: true, // activer les subscriptions
listChanged: true, // notifier quand la liste change
},
},
}
)
// Gérer les subscriptions
import {
SubscribeRequestSchema,
UnsubscribeRequestSchema,
} from '@modelcontextprotocol/sdk/types.js'
const subscribers = new Map<string, Set<string>>() // uri → set de session IDs
server.setRequestHandler(SubscribeRequestSchema, async (request) => {
const { uri } = request.params
if (!subscribers.has(uri)) {
subscribers.set(uri, new Set())
}
// ... enregistrer l'abonnement
return {}
})
server.setRequestHandler(UnsubscribeRequestSchema, async (request) => {
const { uri } = request.params
subscribers.delete(uri)
return {}
})
// Notifier les abonnés quand le contenu change
async function notifyResourceUpdated(uri: string) {
await server.notification({
method: 'notifications/resources/updated',
params: { uri },
})
}
// Exemple : surveiller un fichier et notifier
import { watch } from 'fs'
watch('/var/log/app.log', async () => {
await notifyResourceUpdated('app://logs/recent')
})Les subscriptions sont utiles pour des métriques en temps réel, des logs streamés, ou tout contenu qui change fréquemment.
| Contenu | MIME type |
|---|---|
| JSON structuré | application/json |
| Texte / logs | text/plain |
| Markdown | text/markdown |
| HTML | text/html |
| CSV | text/csv |
| Image PNG (base64) | image/png |
Pour les ressources binaires (images), utiliser blob à la place de text :
return {
contents: [{
uri,
mimeType: 'image/png',
blob: imageBuffer.toString('base64'), // base64
}],
}Si votre serveur ajoute ou retire des resources dynamiquement :
await server.notification({
method: 'notifications/resources/list_changed',
params: {},
})Les clients rechargent la liste des resources disponibles.
Tools pour agir, resources pour lire. Le chapitre suivant ajoute les prompts — des templates de conversation réutilisables que Claude peut instancier avec des variables.