mcp-fantasy-library

loickaczmarek/mcp-fantasy-library

3.2

If you are the rightful owner of mcp-fantasy-library and would like to certify it and/or have it hosted online, please leave a comment on the right or send an email to henry@mcphub.com.

This document provides a comprehensive overview of setting up a Model Context Protocol (MCP) server using Typescript, focusing on creating a 'stdio' type server that interfaces with a local database.

Tools
1
Resources
0
Prompts
0

Tutorial MCP Server Typescript

L'objectif de ce tutoriel est de créer un premier server MCP de type stdio en Typescript, et qui donne accÚs à une base de donnée locale.

Pour avoir plus d'informations sur ce qu'est le protocole MCP : https://modelcontextprotocol.io/introduction

Prérequis

  • Avoir des bases en javascript
  • Avoir Node.js
  • Avoir Docker

Initialisation

On va commencer pas initialiser le projet. Pour ça, nous pouvons commencer par explorer le fichier package.json.

Il nous manque le fichier d'entrée src/index.ts : il faut donc le créer

Et nous pourrons lancer la commande d'installation des packages de notre choix.

Avec npm : npm install

Premiers pas

Nous allons ajouter le minimum nécessaire pour faire tourner le server MCP. Pour ça, dans index.ts, ajouter ceci :

const server = new Server(
    {
        name: "Fantasy Library Database",
        version: "0.1.0",
    },
    {
        capabilities: {
            resources: {},
            tools: {},
        },
    },
);

async function runServer() {
    const transport = new StdioServerTransport();
    console.log("Starting MCP Fantasy Library server")
    await server.connect(transport);
}

runServer().catch(console.error);

Nous allons construire et lancer l'application :

npm run build && npm run dev

Si tout se passe bien, vous devriez voir apparaĂźtre le message suivant : Starting MCP Fantasy Library server

Quelques concepts

Les servers MCP permettent d'exposer des outils, des ressources, des prompts, etc ... ( voir la documentation ici )

Pour l'exercice, nous allons nous concentrer sur deux concepts :

  • l'outil : permet d'effectuer une action prĂ©cise avec ou sans paramĂštres. L'usage est stateless, pour un usage ponctuel, et qui n'a pas besoin de garder une connection active, par exemple. C'est comparable Ă  un appel prĂ©cis d'une API.
  • la ressource : permet de donner des accĂšs. L'usage est stateful, l'usage est prĂ©vu pour garder une connection ouverte. C'est comparable Ă  une dĂ©claration d'API.

Pour utiliser le server MCP, nous allons injecter des JSON au format JSON RPC, qui est le format utilisé par le protocole MCP.

Utilisation outil

Mise en place

Nous avons besoin d'une base de donnée, et pour ça, nous allons utiliser le docker-compose.yml

lancer docker-compose up -d

Vérifier que la base de donnée est accessible via n'importe quel outil ( exemple d'outil : DBeaver)

Création du tool

Exposition

Dans un premier temps, nous allons rendre visible les outils de notre server. Il faut donc ajouter un handler de requĂȘtes de listing d'outil.

Nous allons aussi en profiter pour déclarer notre premier outil : récupérer la description d'un livre

Dans index.ts, il faut ajouter :

// au niveau des imports
import {
    ListToolsRequestSchema
} from "@modelcontextprotocol/sdk/types.js";

...

// avant de lancer la fonction runServer()
server.setRequestHandler(ListToolsRequestSchema, async () => {
    return {
        tools: [
            {
                name: "get-book-description",
                description: "Get the book description from fantasy library",
                inputSchema: {
                    type: "object",
                    properties: {
                        name: { type: "string" },
                    },
                },
            },
        ],
    };
});

Lancer le build : npm run build

Puis, vérifier que ça fonctionne avec la method tools\list :

( cat <<\EOF
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","clientInfo":{"name":"example-client","version":"1.0.0"},"capabilities":{}}}
{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
EOF
) | npm run dev

Parmi la liste des outils, l'outil de rĂ©cupĂ©ration de livre devrait ĂȘtre visible dans le rĂ©sultat. La description de l'outil est importante : c'est cette description qui sera utilisĂ©e par les LLM pour identifier le bon outil !

Utilisation

Maintenant que l'outil est listé, nous allons l'utiliser.

Ajoutons ceci :

// import
import {
    CallToolRequestSchema,
    ListToolsRequestSchema
} from "@modelcontextprotocol/sdk/types.js";

// code
server.setRequestHandler(CallToolRequestSchema, async (request) => {
    if (request.params.name === "get-book-description") {
        return {
            content: [{ type: "text", text: "Hello World !" }],
            isError: false,
        };
    }
    throw new Error(`Unknown tool: ${request.params.name}`);
});

Build le code et lancer avec la method tools\call :

( cat <<\EOF
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","clientInfo":{"name":"example-client","version":"1.0.0"},"capabilities":{}}}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get-book-description", "arguments" : {}}}
EOF
      ) | npm run dev

Si tout se passe bien, le message "Hello world !" devrait apparaĂźtre en sortie.

Connection Ă  la base

Il est temps de connecter le server à la base de données.

Pour ça, on va ajouter le nécessaire :

  • un argument pour passer l'url local
  • la gestion de la connection
  • la requĂȘte Ă  exĂ©cuter

Dans un premier temps, on va ajouter :

// import
import * as pg from "pg";

// code
const args = process.argv.slice(2);
if (args.length === 0) {
    console.error("Please provide a database URL as a command-line argument");
    process.exit(1);
}

const databaseUrl = args[0];

const resourceBaseUrl = new URL(databaseUrl);
resourceBaseUrl.protocol = "postgres:";
resourceBaseUrl.password = "";

const pool = new pg.Pool({
    connectionString: databaseUrl,
});

cette partie sert à ouvrir une connection à la base de donnée.

Créons également une méthode qui sera utilisé par l'outil :

async function getBookDescription(request : any) {
    const bookName = request.params.arguments?.name as string;

    const client = await pool.connect();
    try {
        await client.query("BEGIN TRANSACTION READ ONLY");
        const result = await client.query(`SELECT description FROM livres WHERE titre = '${bookName}'`);
        return {
            content: [{ type: "text", text: JSON.stringify(result.rows, null, 2) }],
            isError: false,
        };
    } catch (error) {
        throw error;
    } finally {
        client
            .query("ROLLBACK")
            .catch((error) =>
                console.warn("Could not roll back transaction:", error),
            );

        client.release();
    }
}

Remplaçons la méthode :

// avant
server.setRequestHandler(CallToolRequestSchema, async (request) => {
    if (request.params.name === "get-book-description") {
        return {
            content: [{ type: "text", text: "Hello World !" }],
            isError: false,
        };
    }
    throw new Error(`Unknown tool: ${request.params.name}`);
});

// aprĂšs
server.setRequestHandler(CallToolRequestSchema, async (request) => {
    if (request.params.name === "get-book-description") {
        return await this.getBookDescription(request);
    }
    throw new Error(`Unknown tool: ${request.params.name}`);
});

Ensuite, il faut builder et valider avec la method tools\call :

( cat <<\EOF
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","clientInfo":{"name":"example-client","version":"1.0.0"},"capabilities":{}}}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get-book-description", "arguments" : {"name":"Culture Code"}}}
EOF
    ) | npm run dev -- postgresql://admin:password@localhost:5432/bibliotheque

Une description brĂšve de Culture Code devrait apparaĂźtre !

Pour aller plus loin

Ajouter des tools pour :

  • la liste des livres disponibles
  • identifier la personne qui a empruntĂ© le livre donnĂ©

Utilisation ressource

Exposition

Comme pour les outils, il faut pouvoir identifier les ressources disponible Il faut donc ajouter un handler de requĂȘtes de listing de ressources.

// imports
import {
    CallToolRequestSchema,
    ListToolsRequestSchema,
    ListResourcesRequestSchema
} from "@modelcontextprotocol/sdk/types.js";

...

// code
const SCHEMA_PATH = "schema";

server.setRequestHandler(ListResourcesRequestSchema, async () => {
    const client = await pool.connect();
    try {
        const result = await client.query(
            "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'",
        );
        return {
            resources: result.rows.map((row) => ({
                uri: new URL(`${row.table_name}/${SCHEMA_PATH}`, resourceBaseUrl).href,
                mimeType: "application/json",
                name: `"${row.table_name}" database schema`,
            })),
        };
    } finally {
        client.release();
    }
});

Lancer le build : npm run build

Puis, vĂ©rifier que ça fonctionne, nous allons requĂȘter avec la method resources/list :

( cat <<\EOF
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","clientInfo":{"name":"example-client","version":"1.0.0"},"capabilities":{}}}
{"jsonrpc":"2.0","id":2,"method":"resources/list","params":{}}
EOF
   ) | npm run dev -- postgresql://admin:password@localhost:5432/bibliotheque
Utilisation

Nous allons ajouter le handler d'utilisation de ressources :

// import
import {
    CallToolRequestSchema,
    ListResourcesRequestSchema,
    ListToolsRequestSchema,
    ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";

// code
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
    const resourceUrl = new URL(request.params.uri);

    const pathComponents = resourceUrl.pathname.split("/");
    const schema = pathComponents.pop();
    const tableName = pathComponents.pop();

    if (schema !== SCHEMA_PATH) {
        throw new Error("Invalid resource URI");
    }

    const client = await pool.connect();
    try {
        const result = await client.query(
            "SELECT column_name, data_type FROM information_schema.columns WHERE table_name = $1",
            [tableName],
        );

        return {
            contents: [
                {
                    uri: request.params.uri,
                    mimeType: "application/json",
                    text: JSON.stringify(result.rows, null, 2),
                },
            ],
        };
    } finally {
        client.release();
    }
});

Lançons le build, vérifions que ça fonctionne avec la method resources/read :

( cat <<\EOF
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","clientInfo":{"name":"example-client","version":"1.0.0"},"capabilities":{}}}
{"jsonrpc":"2.0","id":2,"method":"resources/read","params":{"uri":"postgres://admin@localhost:5432/clients/schema"}}
EOF
    ) | npm run dev -- postgresql://admin:password@localhost:5432/bibliotheque

Le rĂ©sultat devrait ĂȘtre les informations de schema de la base de donnĂ©e

Pour aller plus loin

Ce tutoriel est basé sur le server MCP Postgres ( ici )

Pour le tester en condition réel, l'idéal est de le brancher sur un client MCP. La liste se trouve là : https://modelcontextprotocol.io/clients

Ce server a été testé et validé avec le client Claude Code et retourne des résultats satisfaisants avec des prompts comme donnes moi la description du livre Culture Code