Deno で RooCode 用にローカルMCPサーバーをさっと作る
やりたいこと
- 外部MCPではなく、その場でサクッと書いた deno スクリプト書いて使わせたい。
- ローカル MCP は 標準入出力でJSONを喋るだけで作れる
- でもテストするのが面倒なので、インメモリでテストしておきたい
これらを、Roo Code でいつでもできるように、手順を整理しておきます。
TODO: 本家 Cline のやり方はあとで確認
知っておくべきこと
- Cline/Roo/ClaudeDesktop はアプリケーション起動時に設定を読み、mcpServer のプロセスを起動して握る
- 明示的にリロードさせないと反映されない(面倒くさい)
簡単なMCPサーバーを実装する
まず、getStringLength
という、与えられた文字列の length を返すだけの簡単なサーバーを実装します。
(MCPとしての動作確認のためだけの実装です)
どこでもいいですが、 ~/mcp/server.ts
か何かで deno script を書きます。
import { Server } from "npm:@modelcontextprotocol/sdk@1.5.0/server/index.js";
import { StdioServerTransport } from "npm:@modelcontextprotocol/sdk@1.5.0/server/stdio.js";
import {
CallToolRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
Tool,
CallToolRequest,
} from "npm:@modelcontextprotocol/sdk@1.5.0/types.js";
const TOOLS: Tool[] = [
{
name: "getStringLength",
description: "Get the length of a string",
inputSchema: {
type: "object",
properties: {
input: { type: "string", descrption: "The input string" },
},
required: ["input"],
},
},
];
const server = new Server(
{
name: "local",
version: "0.1.0",
},
{
capabilities: {
resources: {},
tools: {
getStringLength: TOOLS[0],
},
},
}
);
server.setRequestHandler(ListResourcesRequestSchema, () => ({
resources: [],
}));
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: TOOLS }));
server.setRequestHandler(CallToolRequestSchema, (request: CallToolRequest) => {
const name = request.params.name;
const args = request.params.arguments ?? {};
switch (name) {
case "getStringLength": {
const input = args.input as string;
if (typeof input !== "string") {
return {
content: [
{
type: "text",
text: `Expected input to be a string, got ${typeof input}`,
},
],
isError: true,
};
} else {
console.error("[response]", input, input.length);
return {
content: [
{
type: "text",
text: `${Array.from(input).length}`,
},
],
isError: false,
};
}
}
default: {
return {
content: [
{
type: "text",
text: `Unknown tool: ${name}`,
},
],
isError: true,
};
}
}
});
await server.connect(new StdioServerTransport());
console.error("MCP server running on stdio");
ライブラリにラップされてますが、内部的には tool/call
に対して、レスポンスを返します。
StdioServerTransport は標準入出力を使うので、ログは console.error などで stderr に吐いたりする必要があります。
インメモリでテスト
これを $ deno run -A server.ts
で起動してMCPサーバーに登録する…前に、ローカルで動作確認をしましょう。
同じファイルの In-Source Testing で InMemoryTransport
を使って、今の connect した server に対してクライアントを作って呼び出します。
/// ....元のコード
import { Client } from "npm:@modelcontextprotocol/sdk@1.5.0/client/index.js";
import { InMemoryTransport } from "npm:@modelcontextprotocol/sdk@1.5.0/inMemory.js";
import { expect } from "jsr:@std/expect@1.0.13";
Deno.test("getStringLength", async () => {
const client = new Client(
{
name: "test client",
version: "1.0",
},
{
capabilities: {},
}
);
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await Promise.all([
client.connect(clientTransport),
server.connect(serverTransport),
]);
const result = await client.callTool({
name: "getStringLength",
arguments: {
input: "Hello, world!",
},
});
expect(result).toEqual({
content: [{ type: "text", text: "13" }],
isError: false,
});
});
これで、 $ deno test -A server.ts
でテストします。
$ deno test -A server.ts
Check file:///home/mizchi/mcp/server.ts
running 1 test from ./v0.ts
getStringLength ...
------- output -------
[response] Hello, world! 13
----- output end -----
getStringLength ... ok (2ms)
ok | 1 passed | 0 failed (4ms)
動作確認が出来ました。
Roo Code に登録する
Roo の設定画面に飛びます。左から3番目のハンバーガーメニューみたいなやつです。
最下段の Edit MCP Settings
で、Roo のMCPの設定ファイルを開きます。
自分のWSL環境だと、次のパスになっています。
/home/mizchi/.vscode-server/data/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json
ここで次のように、さっき書いたスクリプトを起動するように設定します。
{
"mcpServers": {
"local": {
"command": "/home/mizchi/.deno/bin/deno",
"args": ["run", "-A", "/home/mizchi/mcp/server.ts"],
"env": {},
"disabled": false,
"alwaysAllow": []
}
}
}
正直 RooCode はパス周りの処理が怪しいので、絶対パスで書いています。
この状態で、Roo の設定画面から MCP Servers 設定をリロードさせると、認識されます。
うまく行っていれば、ツール名や parameters が表示されるはずです。
(※このスクリーンショットは、後述の別のセットアップ済みの状態の設定です)
ここで実際に Roo Code から呼び出してみます。
うまく動きましたね。
おまけ: 自分用のヘルパを書いた @mizchi/mcp-helper
新しくツールを設定するのに、たぶん本当にやることはこれだけのはず。
- JSONSchema を書く
- その型を満たすハンドラを書く
それ以外をラップして TypeScript で型がつきやすくするラッパーを書きました。
中でやってるのは zod でスキーマ書いて、zod-to-jsonschema でスキーマを生成しつつ、zod.infer で型を推論してからハンドラーに渡す、というだけです。
(余談ですが、このREADMEはこの実装自体のテストコードを読ませてRooCodeに生成させました)
これを使い、2つのツールを実装してみます。
- readUrl: 与えられたURLの本文抽出して, markdown に変換
- getFileSize: read-file する前にサイズを確認する
import { createToolsServer } from "jsr:@mizchi/mcp-helper";
import { StdioServerTransport } from "npm:@modelcontextprotocol/sdk@1.5.0/server/stdio.js";
import { z } from "npm:zod@3.24.2";
import { getExtractContent } from "./lib.ts";
function trim(strings: TemplateStringsArray, ...values: any[]) {
const result = String.raw({ raw: strings }, ...values);
return result
.split("\n")
.map((s) => s.trim())
.join("\n");
}
const tools = [
{
name: "readUrl",
description: trim`
URLを読み込み本文を抽出した結果を Markdown にします。
`,
inputSchema: z.object({
url: z.string().describe("The URL to read"),
}),
outputSchema: z.string(),
},
{
name: "getFileSize",
description: trim`
ファイルサイズを取得します。
ファイルサイズが大きい .json やバイナリの可能性が高いファイルを read-file する前に、
なるべくファイルサイズを確認してください。
`,
inputSchema: z.object({
filePath: z.string().describe("The path to the file"),
}),
outputSchema: z.number(),
},
] as const;
// Create the server with type-safe handlers
const server = createToolsServer(
{
name: "local",
version: "1.0.0",
},
tools,
{
async readUrl(params: { url: string }) {
const data = await fetch(params.url).then((res) => res.text());
const md = getExtractContent(data);
return md;
},
async getFileSize(params: { filePath: string }) {
const data = await Deno.stat(params.filePath);
return data.size;
},
}
);
await server.connect(new StdioServerTransport());
おわり
簡単にローカルMCPサーバーをポンポン作れる環境ができました。
今回実装したツール自体は単純で、もう少し作り込まないといけないとは思うんですが、やりたいときにさっと作れるということで心理的な障壁を下げとくのがいいと思います。
Discussion