0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MCPサーバーを使って請求書作成から送付まで自動化してみた話

Last updated at Posted at 2025-05-16

はじめに

こんにちは
株式会社BTMでエンジニアをしている島谷です。

現在、他社様と副業契約を結んでおり、毎月月初に請求書を作成して送付しています。
その作業が毎回手間に感じられ、「もっとスマートに作成できないか」と考えるようになりました。

そこで、話題の Model Context Protocol(以下、MCP)を使って、請求書作成から送付までの自動化に試みました。

背景

これまで、月初になると手作業で次のような手順を踏んでいました。

  1. Toggl Track で前月分の合計作業時間を控える
  2. Googleスプレッドシートの「請求書テンプレート」シートを複製
  3. 複製したシートに合計作業時間および請求日、支払い期限等の日付を入力
  4. PDFとしてエクスポート
  5. 担当者宛てにGmailで送信

流れ自体は単純ですが、シートへの入力ミスを防ぐために何度も確認していることから、10〜15分ほど取られていました。

そこで、「MCPサーバーを使って各サービスを連携させれば、AIチャットに『先月分の請求書を作って送って』とひと言指示するだけで、一連の処理を完結できるのではないか」と考えました。

結果として、請求書作成から送付までを自動化できたので、成果物と自動化までの過程を紹介します。

出来上がったもの

請求書作成キャプチャ.gif

わかりにくいですが、プロンプトを投げた後、以下の処理を自動で行ってます。

  • Toggl Trackから作業時間を取得
  • templateシートを複製
  • 複製したシートの名称を変更
  • 作業時間および請求日等を入力
  • PDFエクスポート
  • Gmail作成

請求書の内容は全てサンプルとなっています。

デモ用のためメール送信は行わず、宛先を空欄にし下書き保存しています。

投げたプロンプト
toggle-toolsのMCPサーバーを使って以下のことをしてほしい。
・togglApiTokenはmcp.jsonに記載している値を対象とする
・2025年4月の合計作業時間を取得して

Google SheetsのMCPサーバーを使って以下のことをしてほしい。
・spreadsheetIdはmcp.jsonに記載している値を対象とする
・「template」シートを複製して

GAS ToolboxのMCPサーバーを使って以下のことをしてほしい。
・spreadsheetIdやrenameSheetBaseUrlはmcp.jsonに記載している値を対象とする
・上記で複製したシートの名前を「25.05」に変更して

再度、Google SheetsのMCPサーバーを使って以下のことをしてほしい。
・「25.05」シートのシートIDを取得して
・上記のシートIDを使用して、K10セルに合計作業時間をhh:mm:ss形式でセットして
・上記のシートIDを使用して、K17セルに「'2025年4月」をセットして
・上記のシートIDを使用して、K18セルに「2025年5月1日」をセットして
・上記のシートIDを使用して、K19セルに「2025年5月31日」をセットして

GAS ToolboxのMCPサーバーを使って以下のことをしてほしい。
・createSheetPdfBaseUrlはmcp.jsonに記載している値を対象とする
・上記で複製したシートをPDFにしてほしい。ファイル名は「請求書(25.04)」として

GmailのMCPサーバーを用いて、以下のことをしてほしい。
・宛先は空欄とする
・上記のダウンロードリンクを添付ファイルとしてメールを下書き保存で作成して
・添付ファイル名は「請求書(25.04)」として

アーキテクチャは以下の通りです。
image.png

作成したソースコードは以下の通りです。

自動化までの過程

まず、自動化する上で関わってくる、MCPホストとMCPクライアント、MCPサーバーの役割について簡単に説明しておきます。

名称 説明
MCPホスト “LLMを動かすアプリ本体” で、ユーザーからの入力を受け取り、内部でMCPクライアントを起動します。
MCPクライアント ホストとサーバーの間に立つプロキシで、ホスト側からのリクエストをサーバーへ送り、返ってきたレスポンスをホストに中継する役目を担います。
MCPサーバー クライアントに対して特定のデータソースやツールへのアクセスを提供します。

今回、CursorをMCPホストとして使用してみます。

Toggl Trackとの連携

前月分の作業時間を取得できるようにするため、Toggl TrackのMCPサーバーが公開されているか調査しました。

調査の結果、Pipedreamにありました。

Pipedreamは、 2,500を超える統合アプリすべてに専用のMCP(モデルコンテキストプロトコル)サーバーを提供しています。これらのサーバーにより、ClaudeのようなAIアシスタントは、標準化された通信プロトコルを介して数千ものAPIに安全にアクセスし、対話することができます。そして、お客様のアカウントに接続することで、現実世界のタスクを実行できます。

MCPサーバーの公開URLとOAuthトークンをコピーして貼り付けるだけでホストと接続できるため、導入がとても手軽です。そこで今回の自動化においては、PipedreamのMCPサーバーを採用することにしました。

Toggl TrackのMCPサーバーの仕様は以下の通りです。
image.png

Available toolsに「Get Time Entries」アクションがあるので、こちらが使えそうです。
image.png

Toggl Trackが提供している「Get Time Entries」は 指定した期間に記録された作業時間を一覧で取得するエンドポイントです。
たとえば、クエリパラメータで「2025年4月1日 〜 4月30日」を指定すると、その期間に記録されたすべての作業時間を取得できます。

しかし、この記事を執筆している時点では、下記のGitHub Issuesにもあるように、MCPサーバー側の「Get Time Entries」アクションはリクエストパラメータ(開始日・終了日など)を受け付けません。そのため「2025年4月1日 〜 4月30日」といった期間を指定しても絞り込めず、期間を限定した作業時間を取得できないことがわかりました。

2025年5月16日時点で上記IssueはClose済です。

そこで、上記を解決するMCPサーバーを一旦自作することにしました。

MCPサーバー自作

MCPサーバーを作成するのに色々調査していたら、以下リポジトリを見つけたので、これを使って自作してみます。

ツール定義

server.tsを以下のように編集します。

api/server.ts
import { z } from "zod";
import { initializeMcpApiHandler } from "../lib/mcp-api-handler";
import { TogglTrackClient } from "../lib/toggle-track";

const handler = initializeMcpApiHandler(
  (server) => {
    server.tool("getTimeEntries", {  // ツールとして、「getTimeEntries」を定義します。
      togglApiToken: z.string(), // Toggl TrackのAPIトークン
      startDate: z.string(), // 開始日
      endDate: z.string() // 終了日
    }, async ({ togglApiToken, startDate, endDate }) => {
      if (!togglApiToken) {
        throw new Error("Unauthorized: Invalid Toggle API key");
      }
      const togglTrackClient = new TogglTrackClient(togglApiToken);
      // 開始日~終了日の期間に記録されたすべての作業時間を取得
      const data = await togglTrackClient.getTotalMonthlyDuration(startDate, endDate);
      return {
        content: [{ type: "text", text: `Result: ${JSON.stringify(data)}` }],
      };
    });
  },
  {
    capabilities: {
      tools: { // ツールの説明
        getTimeEntries: {
          description: "Get span time entries",
        },
      },
    },
  }
);

export default handler;

作業時間の取得

以下のファイルを新規作成し、Toggl Trackの「Get Time Entries」を呼び出すように実装します。

lib/toggl-track.ts
import axios from 'axios';

const TOGGL_TRACK_API_BASE_URL = 'https://api.track.toggl.com/api/v9';

// 省略 //

export class TogglTrackClient {
  private apiToken: string;

  constructor(apiToken: string) {
    this.apiToken = apiToken;
  }

  private getAuthHeader() {
    return {
      'Content-Type': "application/json",
      Authorization: `Basic ${Buffer.from(`${this.apiToken}:api_token`).toString('base64')}`,
    };
  }

  async getTotalMonthlyDuration(inputStartDate?: string, inputEndDate?: string) {
    const now = new Date();
    const start = inputStartDate ? inputStartDate : `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-01`;
    const end = inputEndDate ? inputEndDate : `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate()}`;

    const { startUTC } = toJSTDayRangeUTC(start);
    const { endUTC: endOfEndUTC } = toJSTDayRangeUTC(end);

    console.log(`startDate: ${startUTC}`);
    console.log(`endDate: ${endOfEndUTC}`);

    // Toggl Trackの「Get Time Entries」を呼び出し
    const response = await axios.get(`${TOGGL_TRACK_API_BASE_URL}/me/time_entries`, {
      headers: this.getAuthHeader(),
      params: {
        start_date: startUTC,
        end_date: endOfEndUTC,
      },
    });

    // 作業時間の合計(秒)を算出して、hh:mm:ss形式に変換する
    const totalSeconds = this.getTotalDurationSeconds(response.data);
    const formattedTime = this.formatSecondsToHHMMSS(totalSeconds);
    return formattedTime;
  }

  // 省略 //

}

Vercelへのデプロイ

Vercelへログインし、デプロイ対象となるリポジトリを選択し、「Import」をクリックします。
image.png

デプロイが完了すると、以下の通りになります。
image.png

CursorでMCPの設定

Cursor > 基本設定 > Cursor Settingsをクリックします。
image.png

「+ Add new global MCP server」をクリックします。
image.png

mcp.jsonが作成されるので、以下のように記述します。

mcp.json
{
  "mcpServers": {
    "toggle-tools": {
      "url": "https://toggle-mcp-with-vercel-functions.vercel.app/sse",
      "togglApiToken": "{{ 自身のtogglのAPIトークン }}"
    }
  },
  "default": "toggle-tools"
}

Cursor SettingsのMCP Serversに表示されればOKです。

image.png

動作確認

実際に、Toggl Trackから合計作業時間を取得できるか試してみます。

image.png

無事に取得できました。

Googleスプレッドシートとの連携

次に、Googleスプレッドシートに対して以下の処理を自動化します。

  • 「template」シートの複製
  • 複製したシート名の変更
  • 複製したシートに前月の作業時間および請求日等の日付を入力
  • PDF出力

まず、Toggl Trackの時と同様に、PipedreamにGoogleスプレッドシートのMCPサーバーが存在するか検索したところ、ありました。

image.png

Available toolsに「Copy Worksheet」アクションと「Update Cell」アクションがあるので、シートの複製やセルへの入力は、これらが使えそうです。
image.png

image.png

PDFとしてエクスポートできるアクションはないため、PDF出力機能を自前で用意し、MCPサーバーから呼び出せるように設定する必要があります。

さらに、「Copy Worksheet」アクションはシートをそのまま複製する機能で、複製後のシート名を変更できません。
たとえば「template」シートをコピーすると、自動的に「template のコピー」という名前で複製されます。

つまり、複製後のシート名を変えたい場合は、シート名を変更する機能を同様に用意しておき、こちらもMCPサーバー経由で呼び出せるようにしておく必要があります。

今回はGoogleスプレッドシートを使っているため、これら二つの機能(PDF出力とシート名の変更)の実装には、親和性の高いGoogle Apps Script(GAS)を採用しました。

CursorでMCPの設定(Google Sheeets MCP Server)

「Sign In」ボタンをクリックし、Pipedreamにログインした後、「Connect account」ボタンをクリックします。
クリック後、以下のような画面になります。

image.png

Cursorのmcp.jsonに「MCP server config」の内容を追記します。

mcp.json
{
  "mcpServers": {
    "toggle-tools": {
      "url": "https://toggle-mcp-with-vercel-functions.vercel.app/sse",
      "togglApiToken": "{{ 自身のToggl TrackのAPIトークン }}"
    },
    "Google Sheets": {
      "url": "https://mcp.pipedream.net/{{ 機密トークン }}/google_sheets",
      "spreadsheetId": "{{ 対象のスプレッドシートのID }}"
    }
  },
  "default": "toggle-tools"
}

spreadsheetIdを定義している理由は、どのスプレッドシートに対して操作するかを指定するためです。

Cursor SettingsのMCP Serversに「Google Sheets」が表示されればOKです。

image.png

MCPサーバー自作

Toggl Trackの時と同様に、これを使って自作します。

シート名の変更やPDF出力といったGASの処理が中心となるため、これらをひとまとめに扱えるMCPサーバー「GAS Toolbox」として作成していきます。

ツール定義(シート名の変更)

server.tsを以下のように編集します。

api/server.ts
import { z } from "zod";
import { initializeMcpApiHandler } from "../lib/mcp-api-handler";
const { renameSheetViaGas } = require("../lib/rename-sheet");

const handler = initializeMcpApiHandler(
  (server) => {
    server.tool( // ツールとして、「renameSheet」を定義します。
      "renameSheet",
      {
        renameSheetBaseUrl: z.string(), // GASのWeb Apps URL
        spreadsheetId: z.string(), // 対象のスプレッドシートID
        oldSheetName: z.string(), // 変更前のシート名
        newSheetName: z.string(), // 変更後のシート名
      },
      async ({ renameSheetBaseUrl, spreadsheetId, oldSheetName, newSheetName }) => {
        try {
          // シート名を変更するGASを呼び出します 
          const response = await renameSheetViaGas({ renameSheetBaseUrl, spreadsheetId, oldSheetName, newSheetName });
          return {
            content: [
              { type: "text", text: `シート名を変更しました: ${oldSheetName} -> ${newSheetName}` },
            ],
          };
        } catch (e: any) {
          return {
            content: [
              { type: "text", text: `Error: ${e.message}` },
            ],
          };
        }
      }
    );
  },
  {
    capabilities: {
      tools: {
        renameSheet: {
          description: "Rename a Google Sheet",
        }
      },
    },
  }
);

export default handler;

ツール名を変更するGASの呼び出し

以下のファイルを新規作成し、ツール名を変更するGASを呼び出すように実装します。

lib/rename-sheet.ts
const renameSheetFetch = require('node-fetch');

/**
 * GAS Web APIのdoPostでシート名を変更
 */
async function renameSheetViaGas({ renameSheetBaseUrl, spreadsheetId, oldSheetName, newSheetName }): Promise<string> {
  const headers = { 'Content-Type': 'application/json' };
  const body = JSON.stringify({ spreadsheetId, oldSheetName, newSheetName });

  const res = await renameSheetFetch(renameSheetBaseUrl, {
    method: 'POST',
    headers,
    body,
  });
  if (!res.ok) throw new Error(`GAS API error: ${res.statusText}`);
  return res.text();
}

module.exports = { renameSheetViaGas }; 

ツール定義(PDF出力)

server.tsを以下のように編集します。

api/server.ts
import { z } from "zod";
import { initializeMcpApiHandler } from "../lib/mcp-api-handler";
const { fetchPdfFromGas } = require("../lib/sheet-to-pdf");

const handler = initializeMcpApiHandler(
  (server) => {
    server.tool(
      "createSheetPdf", // ツールとして、「createSheetPdf」を定義します。
      {
        createSheetPdfBaseUrl: z.string(), // GASのWeb Apps URL
        spreadsheetId: z.string(), // 対象のスプレッドシートID
        sheetName: z.string(), // PDF出力対象のシート名
        downloadFileName: z.string(), // PDFファイル名
      },
      async ({ createSheetPdfBaseUrl, spreadsheetId, sheetName, downloadFileName }) => {
        try {
          // PDF出力するGASを呼び出します 
          const downloadUrl = await fetchPdfFromGas({ createSheetPdfBaseUrl, spreadsheetId, sheetName, downloadFileName });
          return {
            content: [
              { type: "text", text: `ダウンロードリンク: ${downloadUrl}` },
            ],
          };
        } catch (e: any) {
          return {
            content: [
              { type: "text", text: `Error: ${e.message}` },
            ],
          };
        }
      }
    );
  },
  {
    capabilities: {
      tools: {
        createSheetPdf: {
          description: "Create a PDF from a Google Sheet",
        }
      },
    },
  }
);

export default handler;

PDF出力するGASの呼び出し

以下のファイルを新規作成し、PDF出力するGASを呼び出すように実装します。

lib/sheet-to-pdf.ts
const nodeFetch = require('node-fetch');

/**
 * GAS Web APIのdoGetでPDFを生成およびダウンロードリンクを取得
 */
async function fetchPdfFromGas({ createSheetPdfBaseUrl, spreadsheetId, sheetName, downloadFileName }) {
  const url = `${createSheetPdfBaseUrl}?spreadsheetId=${encodeURIComponent(spreadsheetId)}&sheetName=${encodeURIComponent(sheetName)}&downloadFileName=${encodeURIComponent(downloadFileName)}`;
  const headers = {};

  const res = await nodeFetch(url, { headers });
  if (!res.ok) throw new Error(`GAS API error: ${res.statusText}`);
  return res.text();
}

module.exports = { fetchPdfFromGas };

本記事ではGASそのものの作成手順、デプロイ設定、スコープの付与方法などの詳細説明は割愛します。
ここで必要なのは、次の2本のエンドポイントだけです。

  • /renameSheet: 複製直後のシート名を任意の名前へ変更
  • /exportPdf: 指定シートをPDF出力してダウンロードリンクを返却

いずれも「ウェブアプリ」として公開し、MCPサーバー側でHTTPS URLを登録しておけば、本記事のフローは動きます。
GAS の基本的な説明は公式ドキュメントにあるとおりですので、そちらを参照してください。

Vercelへのデプロイ

同様に、Vercelへログインし、デプロイ対象となるリポジトリを選択し、「Import」をクリックします。
image.png

デプロイが完了すると、以下の通りになります。
image.png

CursorでMCPの設定(自作したMCPサーバー)

mcp.jsonへ以下のように記述します。

mcp.json
{
  "mcpServers": {
    "toggle-tools": {
      "url": "https://toggle-mcp-with-vercel-functions.vercel.app/sse",
      "togglApiToken": "{{ 自身のToggl TrackのAPIトークン }}"
    },
    "Google Sheets": {
      "url": "https://mcp.pipedream.net/{{ 機密トークン }}/google_sheets",
      "spreadsheetId": "{{ 対象のスプレッドシートのID }}"
    },
    "GAS Toolbox": {
      "url": "https://gas-toolbox-with-vercel-functions.vercel.app/sse",
      "spreadsheetId": "{{ 対象のスプレッドシートのID }}",
      "createSheetPdfBaseUrl": "{{ PDF出力用GASのWeb Apps URL }}",
      "renameSheetBaseUrl": "{{ シート名変更用GASのWeb Apps URL }}"
    }
  },
  "default": "toggle-tools"
}

Cursor SettingsのMCP Serversに「GAS Toolbox」が表示されればOKです。

image.png

動作確認

実際に、Googleスプレッドシートに対して操作できるか試してみます。
まずは、シートの複製を行ってみます。
image.png

スプレッドシートを確認すると、複製されていることが確認できました。
image.png

次に、シート名の変更を行なってみます。
image.png

スプレッドシートを確認すると、シート名が変更されていることが確認できました。
image.png

次に、指定したセルに対して作業時間や日付等の入力を行なってみます。
image.png

スプレッドシートを確認すると、指定したセルに作業時間や日付等が入力されていることが確認できました。
image.png

次に、PDF出力を行なってみます。
image.png

出力したPDFはGoogle Driveに保存されていることが確認できました。
image.png

PDFも問題なく開くことができます。
image.png

Gmailとの連携

最後に、出力したPDFをGmailで担当者へ自動送信できるようにします。

PipedreamにGmailのMCPサーバーが存在するので、こちらを使用します。

image.png

メールの下書き保存や送信は以下のアクションになります。
image.png
image.png

CursorでMCPの設定

「Connect account」ボタンをクリックした後に表示される、「MCP server config」の内容をCursorのmcp.jsonに追記します。

image.png

mcp.json
{
  "mcpServers": {
    "toggle-tools": {
      "url": "https://toggle-mcp-with-vercel-functions.vercel.app/sse",
      "togglApiToken": "{{ 自身のToggl TrackのAPIトークン }}"
    },
    "Google Sheets": {
      "url": "https://mcp.pipedream.net/{{ 機密トークン }}/google_sheets",
      "spreadsheetId": "{{ 対象のスプレッドシートのID }}"
    },
    "GAS Toolbox": {
      "url": "https://gas-toolbox-with-vercel-functions.vercel.app/sse",
      "spreadsheetId": "{{ 対象のスプレッドシートのID }}",
      "createSheetPdfBaseUrl": "{{ PDF出力用GASのWeb Apps URL }}",
      "renameSheetBaseUrl": "{{ シート名変更用GASのWeb Apps URL }}"
    },
    "Gmail": {
      "url": "https://mcp.pipedream.net/{{ 機密トークン }}/gmail"
    }
  },
  "default": "toggle-tools"
}

Cursor SettingsのMCP Serversに「Gmail」が表示されればOKです。

image.png

動作確認

実際に、メール送信(下書き保存)できるか確認してみます。
image.png

無事に下書き保存されました。
image.png

添付ファイル付きであることも確認できました。
image.png

一連の流れをまとめて実行する

各サービスとの連携設定が済んだら、これまで使ったプロンプトをひとつにまとめて送るだけで、請求書作成から送付までを一括で実行できます。

ただ、Cursorの設定がデフォルトのままだと、Agentはツールを実行するたびユーザに確認を求めてきます。
image.png

Cursor Settings → Features で 「Enable auto-run mode」 にチェックを入れておくと、
Agentでのコマンド実行に対して確認を行わず実行するようになります。
image.png

終わりに

MCPサーバーを活用し、請求書の作成から送付までを自動化する過程を紹介しました。

公開されている MCPサーバーを組み合わせるだけで、「データを取得して別サービスへ渡す」といった連携フローが簡単に構築できます。
今回、シート名の変更やPDF生成など、まだ用意されていない機能は自前で実装しましたが、もしこれらも公式サーバーに追加されれば、完全ノーコードで済むでしょう。

とりわけ魅力なのは導入障壁の低さです。
公開サーバーのセットアップ手順に沿って URLとトークンを登録するだけで連携が完了し、あとは「こうしたい」を自然言語で指示すれば、LLMが適切なツールを選んで実行してくれます。

MCPはまだ新しい仕組みですが、今後の発展を大いに期待しています。

以上です、最後までご覧いただきありがとうございました。

株式会社BTMではエンジニアの採用をしております。
ご興味がある方はぜひコチラをご覧ください。

0
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?