0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

WSH JScriptでMCPサーバーを作成し、Excelを操作させる

Posted at

MCPはJSON-RPCを用いており、標準入出力でやりとりする場合は改行区切りです1。この程度であれば、Microsoft WindowsにプリインストールされているJScriptでも実装できそうです。もっとも、MCPクライアントを用意できる環境ならNode.jsなど、より適したツールをインストールできるでしょうから、なにか特に実用的というわけではありませんが。

WSHで作成するならCOMを活用するのが面白いでしょう。今回はExcelを操作させてみます。

まずはツールとして公開する機能を考えます。JScriptからオブジェクト自体を返すのは難しいため、オブジェクトをプールに登録して、そのIDを返す実装にしました。メソッド/プロパティにあたるツールを実行する際は毎回IDを引数として渡します。

function generateUUID() {
  // ...
}

/** @type {{[key:UUID]:unknown}} */
var pool = {};
/**
 * @param {unknown} obj
 */
function addToPool(obj) {
  var uuid = generateUUID();
  while (pool.hasOwnProperty(uuid)) {
    uuid = generateUUID();
  }
  pool[uuid] = obj;
  return uuid;
}

/**
 * @typedef {UUID&{__excelApplicationBrand:never}} ExcelApplicationID
 * @typedef {UUID&{__excelWorkbooksBrand:never}} ExcelWorkbooksID
 * @typedef {UUID&{__excelWorkbookBrand:never}} ExcelWorkbookID
 * @typedef {UUID&{__excelWorksheetsBrand:never}} ExcelWorksheetsID
 * @typedef {UUID&{__excelWorksheetBrand:never}} ExcelWorksheetID
 * @typedef {UUID&{__excelRangeBrand:never}} ExcelRangeID
 */

/**
 * @overload
 * @param {"Excel.Application"} progId
 * @returns {ExcelApplicationID}
 */
/**
 * @param {string} progId
 * @returns {UUID}
 */
function wscriptCreateObject(progId) {
  return addToPool(WScript.CreateObject(progId));
}

/**
 * https://learn.microsoft.com/office/vba/api/excel.application.quit
 * @param {ExcelApplicationID} excelApplicationId
 */
function excelApplicationQuit(excelApplicationId) {
  // @ts-ignore
  pool[excelApplicationId].Quit();
}

// ...

function test() {
  var excel = wscriptCreateObject("Excel.Application");
  try {
    excelApplicationSetVisible(excel, true);

    var workbooks = excelApplicationGetWorkbooks(excel);
    excelWorkbooksAdd(workbooks);
    if (excelWorkbooksGetCount(workbooks) !== 1) {
      throw new Error(
        "Assertion failed: excelWorkbooksGetCount(workbooks) !== 1"
      );
    }

    var workbook = excelWorkbooksItem(workbooks, 1);
    var worksheets = excelWorkbookGetWorksheets(workbook);
    if (excelWorksheetsGetCount(worksheets) !== 1) {
      throw new Error(
        "Assertion failed: excelWorksheetsGetCount(worksheets) !== 1"
      );
    }
    var worksheet = excelWorksheetsItem(worksheets, 1);

    var cell = excelWorksheetCells(worksheet, 1, 1);
    excelRangeSetValue(cell, "foo");
    if (excelRangeGetValue(cell) !== "foo") {
      throw new Error('Assertion failed: excelRangeGetValue(cell) !== "foo"');
    }
  } finally {
    excelApplicationSetDisplayAlerts(excel, false);
    excelApplicationQuit(excel);
  }
}

続いて最小限のMCPサーバー実装を作成します。npx @modelcontextprotocol/inspector cscript //E:JScript //Nologo server.jsコマンドで動作を観察しながら実装を進めていきます。

Function("return this")().JSON = {
  parse: function (sJSON) {
    return eval("(" + sJSON + ")");
  },
  stringify: (function () {
    // ...
  })()
};

/**
 * https://github.com/modelcontextprotocol/modelcontextprotocol/blob/c87a0da6d8c2436d56a6398023c80b0562224454/schema/2024-11-05/schema.ts#L9
 * @constant
 * @type {"2.0"}
 */
var JSONRPC_VERSION = "2.0";
/**
 * https://github.com/modelcontextprotocol/modelcontextprotocol/blob/c87a0da6d8c2436d56a6398023c80b0562224454/schema/2024-11-05/schema.ts#L8
 * @constant
 * @type {"2024-11-05"}
 */
var LATEST_PROTOCOL_VERSION = "2024-11-05";

/**
 * https://github.com/modelcontextprotocol/modelcontextprotocol/blob/c87a0da6d8c2436d56a6398023c80b0562224454/schema/2024-11-05/schema.ts#L83
 * @constant
 * @type {-32700}
 */
var PARSE_ERROR = -32700;
var INVALID_REQUEST = -32600;
var METHOD_NOT_FOUND = -32601;
var INVALID_PARAMS = -32602;
var INTERNAL_ERROR = -32603;

/**
 * @param {string} level
 * @param {string} message
 */
function log(level, message) {
  WScript.StdErr.WriteLine(
    "[" + new Date().toString() + "]\t[" + level + "]\t" + message
  );
}

/**
 * @param {unknown} id
 * @param {Record<string,unknown>} response
 */
function respond(id, response) {
  response.jsonrpc = JSONRPC_VERSION;
  /**
   * FIXME
   * https://github.com/modelcontextprotocol/modelcontextprotocol/blob/c87a0da6d8c2436d56a6398023c80b0562224454/schema/2024-11-05/schema.ts#L56
   */
  response.id = typeof id === "number" || typeof id === "string" ? id : "";

  var str = JSON.stringify(response);
  WScript.StdOut.WriteLine(str);
  log("debug", str);
}

/**
 * @constant
 * @type {{name:"jscript-excel-mcp-server";version:"0.1.0";}}
 */
var SERVER_INFO = {
  name: "jscript-excel-mcp-server",
  version: "0.1.0"
};

(function main() {
  while (!WScript.StdIn.AtEndOfStream) {
    var line = WScript.StdIn.ReadLine();
    /** @type {Record<string,unknown>} */
    var request = {};
    try {
      request = JSON.parse(line);
    } catch (e) {
      respond(request.id, {
        error: {
          code: PARSE_ERROR,
          message: "PARSE_ERROR: " + (e instanceof Error && e.message) || ""
        }
      });
      continue;
    }

    // https://github.com/modelcontextprotocol/modelcontextprotocol/blob/c87a0da6d8c2436d56a6398023c80b0562224454/schema/2024-11-05/schema.ts#L21
    if (typeof request.method !== "string") {
      respond(request.id, {
        error: {
          code: INVALID_REQUEST,
          message: "INVALID_REQUEST: " + JSON.stringify(request)
        }
      });
      continue;
    }

    switch (request.method) {
      case "initialize":
        respond(request.id, {
          result: {
            protocolVersion: LATEST_PROTOCOL_VERSION,
            capabilities: { tools: {} },
            serverInfo: SERVER_INFO
          }
        });
        break;

      default:
        respond(request.id, {
          error: {
            code: METHOD_NOT_FOUND,
            message: "METHOD_NOT_FOUND: " + JSON.stringify(request)
          }
        });
    }
  }
})();

起点となるInitializeRequest/InitializeResultの処理をあらかじめ作成しました。MCP Inspectorで[Connect]をクリックすると、[Error output from MCP server]に以下のようなログメッセージが表示されるはずです。

[Wed May 21 00:14:04 UTC+0900 2025] [debug] {"result": {"protocolVersion": "2024-11-05", "capabilities": {"tools": {}}, "serverInfo": {"name": "jscript-excel-mcp-server", "version": "0.1.0"}}, "jsonrpc": "2.0", "id": 0}
[Wed May 21 00:14:04 UTC+0900 2025] [debug] {"error": {"code": -32601, "message": "METHOD_NOT_FOUND: {\"jsonrpc\": \"2.0\", \"method\": \"notifications/initialized\"}"}, "jsonrpc": "2.0", "id": ""}

1行目で、実装したInitializeRequestへの応答が正しく処理されていることがわかります。続いて2行目ではMETHOD_NOT_FOUNDエラーが発生していますが、今回は簡易的な実装なので"notifications/*"のリクエストは無視することにします。

MCP Inspectorで[List Tools]をクリックします。

[Wed May 21 00:18:15 UTC+0900 2025] [debug] {"error": {"code": -32601, "message": "METHOD_NOT_FOUND: {\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"tools/list\", \"params\": {\"_meta\": {\"progressToken\": 1}}}"}, "jsonrpc": "2.0", "id": 1}

"tools/list"に対応する応答を実装しましょう。スキーマではListToolsRequest/ListToolsResultと定義されています。ツールがひとつもなければ以下の記述だけでエラーは解消されます。

switch (request.method) {
  // ...

  case "tools/list":
    respond(request.id, {
      result: {
        tools: []
      }
    });
    break;

  // ...
}

公開するツールごとにToolオブジェクトを追加していきます。

var TOOLS = [
  {
    name: "wscript_create_object",
    inputSchema: {
      type: "object",
      properties: {
        progId: { type: "string" }
      },
      required: ["progId"]
    }
  },
  {
    name: "excel_application_set_visible",
    inputSchema: {
      type: "object",
      properties: {
        excelApplicationId: { type: "string" },
        value: { type: "boolean" }
      },
      required: ["excelApplicationId", "value"]
    }
  }

  // ...
];

list-tools.png

[Tools]に列挙されたツールを選択し、引数を入力して[Run Tool]をクリックすると、次のようなエラーが表示されます。

"MCP error -32601: METHOD_NOT_FOUND: {"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"_meta": {"progressToken": 2}, "name": "wscript_create_object", "arguments": {"progId": "Excel.Application"}}}"

"tools/call"に対応する応答を実装します。スキーマではCallToolRequest/CallToolResultとして定義されています。

/**
 * @constant
 */
var HANDLERS = {
  wscript_create_object: function (args) {
    return wscriptCreateObject(args["progId"]);
  },
  excel_application_set_visible: function (args) {
    return excelApplicationSetVisible(
      args["excelApplicationId"],
      args["value"]
    );
  },
  excel_application_set_display_alerts: function (args) {
    return excelApplicationSetDisplayAlerts(
      args["excelApplicationId"],
      args["value"]
    );
  }

  // ...
};

switch (request.method) {
  // ...

  case "tools/call":
    if (
      typeof request.params !== "object" ||
      request.params === null ||
      typeof request.params["name"] !== "string"
    ) {
      respond(request.id, {
        error: {
          code: INVALID_PARAMS,
          message: "INVALID_PARAMS: " + JSON.stringify(request)
        }
      });
      break;
    }

    try {
      var ret = HANDLERS[request.params["name"]](request.params["arguments"]);
      respond(request.id, {
        result: {
          content: [
            {
              type: "text",
              text: JSON.stringify(ret)
            }
          ]
        }
      });
    } catch (e) {
      respond(request.id, {
        result: {
          content: [
            {
              type: "text",
              message: (e instanceof Error && e.message) || ""
            }
          ],
          isError: true
        }
      });
    }
    break;

  // ...
}

これで、MCP Inspectorからツールを呼び出すまでの一連のやりとりを実装できました。実際にClaude Desktopで使用する場合は、設定ファイルに次のように指定します。

{
  "mcpServers": {
    "jscript-excel-mcp-server": {
      "command": "cscript",
      "args": [
        "//E:JScript",
        "//Nologo", 
        "<path_to>\\server.js"
      ]
    }
  }
}

試してみたところ、日本語は正しく送受信できないようです。文字コードの都合だったと記憶していますが詳しくは調べていません。

claude-excel.png

  1. https://modelcontextprotocol.io/specification/2025-03-26/basic/transports?utm_source=chatgpt.com#stdio

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?