【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】Crawleeを使ってみる(Scraping with…

  2. Node.js

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

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

  4. Node.js

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

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

  6. Node.js

    【NodeJS】Gemini APIを使ってみる

最近の記事

制作実績一覧

  1. Checkeys