【NodeJS】自作MCPサーバー with Gemini CLI

Node.js

1. 概要

前回はGemini APIを使ってみる内容についてでした。今回は自作MCP(Model Context Protocol)サーバーをGemini CLIと連携して使ってみる内容になります。

対象としては開発を1年程やってて自分で最初から開発してみたい方になります。そのため細かい用語などの説明はしません。

2. nodeのインストール

こちらを参考

3. プロジェクトを作成

mkdir fakestore-demo
cd fakestore-demo
npm init
npm i -D typescript @types/node ts-node axios @types/express express
npx tsc --init

4. 必要なライブラリをインストール

npm install @modelcontextprotocol/sdk zod dotenv

5. ソースコード

5-1. package.json

{
  "name": "fakestore-demo",
  "version": "1.0.0",
  "type": "module",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "tsc",
    "prestart": "npm run build",
    "start": "node dist/index.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/express": "^5.0.3",
    "@types/node": "^24.2.0",
    "axios": "^1.11.0",
    "express": "^5.1.0",
    "ts-node": "^10.9.2",
    "typescript": "^5.9.2"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.17.2",
    "dotenv": "^17.2.1"
  }
}

5-2. tsconfig.json

{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "target": "ES2022",
    "sourceMap": true,
    "outDir": "dist",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "types": ["node"]
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

5-3. src/env.ts

export const SERVER_NAME = process.env.SERVER_NAME || "fakestore-mcp-server";
export const DEBUG = process.env.DEBUG;
export const LOG_FILE = process.env.LOG_FILE || "/tmp/fakestore-mcp.log";
export const FAKE_STORE_API =
  process.env.FAKE_STORE_API || "https://fakestoreapi.com/products";

5-4. src/utils.ts

import fs from "node:fs";
import { DEBUG, LOG_FILE } from "./env.js";

export const debug = (...args: unknown[]) => {
  if (DEBUG !== "1") return;
  const line = args
    .map((a) => {
      try {
        return typeof a === "string" ? a : JSON.stringify(a);
      } catch {
        return String(a);
      }
    })
    .join(" ");
  const timestamp = new Date().toLocaleString("ja-JP", {
    timeZone: "Asia/Tokyo",
  });
  fs.appendFileSync(LOG_FILE, `[${timestamp}] ${line}\n`);
};

5-5. src/index.ts

import * as mcp from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import axios from "axios";
import { z } from "zod";
import { DEBUG, FAKE_STORE_API, SERVER_NAME } from "./env.js";
import { debug } from "./utils.js";

interface Product {
  id: number;
  title: string;
  price: number;
  description: string;
  category: string;
  image: string;
  rating: {
    rate: number;
    count: number;
  };
}

const mcpServer = new mcp.McpServer({
  name: SERVER_NAME,
  version: "1.0.0",
});

const fetchProducts = async (minPrice: number): Promise<Product[]> => {
  const response = await axios.get<Product[]>(FAKE_STORE_API);
  const products = response.data;
  return products.filter((product) => product.price >= minPrice);
};

const GetProductsByMinPriceInput = z.object({
  minPrice: z
    .number()
    .default(1000)
    .describe("The minimum price of the products to retrieve."),
});

mcpServer.tool(
  "get_products_by_min_price",
  "Get a list of products from FakeStoreAPI, filtered by a minimum price.",
  GetProductsByMinPriceInput.shape,
  async (args, _extra) => {
    debug(`[get_products_by_min_price] args:`, args);
    const { minPrice } = GetProductsByMinPriceInput.parse(args);
    try {
      const products = await fetchProducts(minPrice);
      let filteredProducts = products.filter(
        (product) => product.price >= minPrice
      );

      if (filteredProducts.length === 0) {
        return { content: [{ type: "text", text: "No products found." }] };
      }

      return {
        content: [{ type: "text", text: JSON.stringify(filteredProducts) }],
      };
    } catch (error) {
      const message =
        error instanceof Error ? error.message : "An unknown error occurred";
      return {
        content: [
          {
            type: "text",
            text: `Error fetching products: ${message}`,
          },
        ],
      };
    }
  }
);

async function main() {
  const transport = new StdioServerTransport();
  await mcpServer.connect(transport);
  if (DEBUG === "1") {
    debug(`MCP server is running...`);
  } else {
    console.error("MCP server is running...");
  }
}

main().catch((error) => {
  if (DEBUG === "1") {
    debug("Server error:", error);
  } else {
    console.error("Server error:", error);
  }
  process.exit(1);
});

5-6. .env

GOOGLE_CLOUD_PROJECT="作成済みのプロジェクト"

5-7. .gemini/settings.json

{
  "mcpServers": {
    "fakestore-mcp-server": {
      "command": "node",
      "args": ["/home/sondon/dev/ai/mcp-server/fakestore-demo/dist/index.js"],
      "env": {
        "SERVER_NAME": "fakestore-mcp-server",
        "DEBUG": "1",
        "LOG_FILE": "/tmp/fakestore-mcp.log",
        "FAKE_STORE_API": "https://fakestoreapi.com/products"
      }
    }
  }
}

5-8. GEMINI.md

出力条件
・表形式
・表のヘッダーは№(連番)、価格、カテゴリ、タイトル

6. 実行

6-1. ビルド

npm run build

6-2. 実行

gemini -p "100ドル以上の商品を取得して高い順から並べて5件だけを出力してください"
| № | 価格 | カテゴリ | タイトル |
|---|---|---|---|
| 1 | 999.99 | electronics | Samsung 49-Inch CHG90 144Hz Curved Gaming Monitor (LC49HG90DMNXZA) – Super Ultrawide Screen QLED  |
| 2 | 695 | jewelery | John Hardy Women's Legends Naga Gold & Silver Dragon Station Chain Bracelet |
| 3 | 599 | electronics | Acer SB220Q bi 21.5 inches Full HD (1920 x 1080) IPS Ultra-Thin |
| 4 | 168 | jewelery | Solid Gold Petite Micropave  |
| 5 | 114 | electronics | WD 4TB Gaming Drive Works with Playstation 4 Portable External Hard Drive |

7. ディレクトリの構造

.
├── .env
├── .gemini
│   └── settings.json
├── GEMINI.md
├── dist
│   ├── env.js
│   ├── env.js.map
│   ├── index.js
│   ├── index.js.map
│   ├── utils.js
│   └── utils.js.map
├── package-lock.json
├── package.json
├── src
│   ├── env.ts
│   ├── index.ts
│   └── utils.ts
└── tsconfig.json

3 directories, 15 files

8. 備考

今回は自作MCP(Model Context Protocol)サーバーをGemini CLIと連携して使ってみる内容でした。

9. 参考

投稿者プロフィール

Sondon
開発好きなシステムエンジニアです。
卓球にハマってます。

関連記事

  1. Node.js

    【NodeJS】TypeScriptでCSVを読み込んで処理する

  2. Node.jsバージョン管理ツールについて調べてみる

  3. Node.js

    【NodeJS】Geminiを使った画像生成(Nano Banana)…

  4. Node.js

    【NodeJS】Crawleeを使ってみる(Scraping with…

  5. Node.js

    【NodeJS】PrismaやTypeScript、MySQLを使って…

  6. 【新米エンジニア学習記録②】TypeScriptの導入

最近の記事

制作実績一覧

  1. Checkeys