mcpField
E
EVS Meter Tools

EVS Meter Tools

リモート
nusaircon-production.up.railway.appその他2今日更新
リポジトリ公式サイト

EVS Meter Tools について

EVS Meter Tools は、対応する NUS と SUTD の電気メーター向けウェブアプリ兼 Telegram ボットです。残高、使用量、チャージ履歴を確認でき、対応する場合はカードでチャージできます。読み取り専用の MCP サーバーは、支払いやカード情報の処理なしで AI アシスタントにメーター情報と支払いリンクを提供します。

主な機能

  1. 01NUS と SUTD の残高確認
  2. 02使用量と最近のチャージ履歴
  3. 03クレジットカードのチャージリンク
  4. 04Telegram ボットとウェブアプリ
  5. 05読み取り専用のメーターツール
  6. 06MCP にカード情報を渡さない設計

活用シーン

  1. 01NUS または SUTD メーターの残高確認
  2. 02最近の使用量とチャージ履歴の確認
  3. 03対応メーターのチャージリンク取得
  4. 04AI アシスタントで残高不足を確認

MCP 対応

公開の読み取り専用 MCP サーバーは、残高、NUS の使用量、最近のチャージ履歴、チャージリンクを取得するツールを提供します。支払いの実行やカード情報の受信はできず、応答に部屋の住所を含めません。

設定

リモートサーバーはインストール不要です。クライアントを選んで設定をコピーするか、ワンクリックで導入できます。

エンドポイントhttps://nusaircon-production.up.railway.app/mcp
インストールコマンドをコピー
Claude CodeTerminal
Cursor.cursor/mcp.json
VS Code.vscode/mcp.json
Claude Desktopclaude_desktop_config.json
Windsurf~/.codeium/windsurf/mcp_config.json

コマンドはターミナルで実行し、JSON は各クライアントの MCP 設定に貼り付けて再起動します。認可が必要なサーバーは初回接続時にログインを求めます。

ツール

詳細

EVS メーターツール

対応している EVS 電力メーター向けの Telegram ボットおよび Web アプリです。対応している NUS および SUTD のメーターはクレジットカードでチャージでき、対応する照会フローでは、利用可能な場合に残高、使用量、チャージ履歴を確認できます。

対応システム

寮グループEVS システム
PGPR、Houses @ PGP、Residential Colleges、NUS College、UTown Residences、RVRC、Valour House を含む NUS の寮cp2nus.evs.com.sg
SUTDSUTDMain / EVSSUTDWebPOS

旧 NUS CP2 フォールバック(cp2.evs.com.sg)は Web アプリとボットから非表示になっています。バックエンドのルートとサービスは保持されていますが、どの UI からもリンクされていません。詳しくはCP2 — 旧 NUS フォールバック(非表示)をご覧ください。

機能

  • Telegram 内でメーター残高、過去 7 日間の使用履歴、最近のチャージを確認
  • 対応している NUS の電力メーターをクレジットカードでチャージ(SGD $6-$50)
  • 対応している SUTD の電力メーターをクレジットカードでチャージ(SGD $10-$50)
  • RSA 暗号化によるカード情報入力 — カード情報が平文でブラウザーの外に出ることはありません
  • Telegram Mini App としても、単独の Web サイトとしても動作
  • システム間ガード:cp2nus のブートストラップは、支払い開始前に、メーターが旧 cp2 システムに属する場合に拒否します
  • SUTD の残高、チャージ履歴、チャージに対応
  • フロー全体で分析情報の追跡とエラーの記録
  • Telegram での残高低下アラート(オプトイン)。チャージを開始するボタン付き
  • 読み取り専用のMCP サーバー。ユーザーが自分の Claude を接続し、独自の指示を設定できます

アーキテクチャ

Telegram Bot (telegraf)          Website (React, /app/)
    │                                │
    ├── /topup                       └── HomePage
    ├── /balance                          └── system selection
    ├── /usage                                + meter ID + amount
    └── /topups
          │ webhook in production/Railway,       │
          │ polling in development               │
          ▼                                       ▼
    Express (server.js)  ←────────────────────────────
          │
          ├── POST /telegram/webhook/* — Telegram webhook receivers
          ├── GET  /webapp              — legacy CP2 (hidden from UI): fetches meter summary, redirects to React
          ├── GET  /webapp/bootstrap    — runs full payment init, returns token
          ├── GET  /webapp/session      — returns session data as JSON for React
          ├── GET  /webapp/pay          — redirects to React card entry page
          ├── POST /webapp/enets_pay    — proxies encrypted card data to eNETS
          ├── POST /webapp/notify       — fallback Telegram payment notification
          └── GET  /webapp/result       — redirects to React result page

    React Frontend (/app/)
          ├── /                 — HomePage (system + meter ID + amount)
          ├── /balance          — HomePage opened in balance-check mode
          ├── /cp2nus           — HomePage preselected for primary NUS CP2NUS flow
          ├── /cp2nus/loading   — cp2nus variant
          ├── /cp2nus/pay       — cp2nus variant
          ├── /cp2nus/result    — cp2nus variant
          ├── /sutd/loading     — SUTD variant
          ├── /sutd/pay         — SUTD variant
          └── /sutd/result      — SUTD variant

支払いフロー

CP2 — 旧 NUS フォールバック(非表示)

非表示。 このフローは Web アプリでもボットでも提供されなくなりました。frontend/src/App.jsx では /app/cp2、/app/loading、/app/pay、/app/result の React ルートがコメントアウトされており、/app/cp2 はサイトマップと robots.txt から除外されています。また、ボットには寮の選択画面が表示されなくなりました。以下のバックエンドのルートとサービスは、参考用および再導入の可能性に備えて保持されています。

旧 EVS WebPOS ポータルからトランザクションを作成し、その後 eNETS を経由します。CP2NUS への移行が済んでいない NUS のメーターにのみ使用されていました。

  1. /webapp — ORE からメーターの住所と残高を取得し、住所と残高をクエリーパラメーターに含めて React の読み込みページにリダイレクトします
  2. ブートストラップ(/webapp/bootstrap)— runPurchaseFlow と getMeterSummary を並行して実行します:
    • GET /EVSWebPOS/ → ログイン → POST /loginServlet(メーターの検証)
    • POST /selectOfferServlet(金額の選択)
    • GET /paymentServlet → merchant_txn_ref を抽出
    • POST creditpayment.jsp → eNETS の message を抽出
    • POST /enets2/PaymentListener.do → RSA 公開鍵(n、e)、netsMid、netsTxnRef を抽出
    • 支払いセッション(有効期間 10 分)を作成し、React のカード入力ページにリダイレクトします
  3. カード入力ページ(/app/pay)— React コンポーネント。/webapp/session 経由でセッションを取得し、eNETS の RSA スクリプトを使ってクライアント側で cardNo + cvv を暗号化します
  4. 支払いプロキシ(/webapp/enets_pay):
    • POST https://www.enets.sg/GW2/uCredit/pay
    • 優先経路: EVS のコールバックフォームを抽出 → POST /EVSWebPOS/transSumServlet → parseEvsTransactionSummary
    • フォールバック: parseEnetsResult で eNETS の領収書 HTML を直接スクレイピング
    • 結果(status、merchantTxnRef、reason)をサーバー側セッションに書き戻します
  5. 結果ページ(/app/result)— React コンポーネント。/webapp/session 経由でサーバーセッションから結果を読み取ります
CP2NUS — NUS の主要なホステルフロー

EVS JSON API と eNETS Payment Page(enetspp)ホストを直接使用します。移行済みメーター向けの主要な NUS ホステルフローです。

  1. /webapp — cp2 と同じです。メーター情報を取得し、/app/cp2nus/loading にリダイレクトします
  2. ブートストラップ(/cp2nus/webapp/bootstrap)— runBootstrap を順に実行します:
    • メーターシステムの確認 — isCp2Meter() ガードにより、WRONG_SYSTEM エラーで cp2 メーターを拒否します。ネットワーク障害時は確認をスキップし、フローを続行します
    • init_pay — POST /enets/init_pay → { txn_identifier, req, sign }
    • meter_info — getMeterSummary → buildPayDisplayAddress
    • enetspp_pay — buildEnetsPayUrl → GET enetspp/pay?p=… → txnReq、keyId、hmac を抽出します
    • TxnReqListener — POST /GW2/TxnReqListener → RSA キー、netsTxnRef、netsMid、paymtNetsMid、txnRand、keyId、hmac
  3. カードページ(/app/cp2nus/pay)— RSA 暗号化は同じです。トップレベルの netsMid ではなく、paymtNetsMid(アクワイアリング MID)を panSubmitForm で使用します
  4. 決済プロキシ(/cp2nus/webapp/enets_pay):
    • GET /GW2/pluginpages/env.jsp → JSESSIONID をシードします
    • POST /GW2/credit/init;jsessionid=…
    • POST /GW2/credit/panSubmitForm
    • 優先パス: 応答の netsTxnStatus → preParsed 結果(b2s 呼び出しなし)
    • フォールバック: POST /enets/b2s → 303 リダイレクト → parsePayResult がパラメーターを base64 デコードします
  5. 結果ページ(/app/cp2nus/result)— サーバーセッションから結果を読み取ります
SUTD

SUTD EVS WebPOS ページと SUTD クレジット決済ブリッジを使用します。

  1. /sutd/webapp — SUTD のメータークレジットを取得し、/app/sutd/loading にリダイレクトします
  2. ブートストラップ(/sutd/webapp/bootstrap)— SUTD WebPOS フローを実行します:
    • メーター ID を指定して POST /EVSSUTDWebPOS/loginServlet
    • 選択した金額を指定して POST /EVSSUTDWebPOS/selectOfferServlet
    • GET /EVSSUTDWebPOS/paymentServlet → merchant_txn_ref を抽出します
    • POST http://120.50.44.233/payment_sutd_credit/creditpayment.jsp
    • POST /enets2/PaymentListener.do → RSA 公開鍵、netsMid、netsTxnRef を抽出します
  3. カードページ(/app/sutd/pay)— NUS の各フローと同じ RSA 暗号化カード入力です
  4. 決済プロキシ(/sutd/webapp/enets_pay)— 暗号化されたカードデータを eNETS に送信し、その後コールバックを /EVSSUTDWebPOS/transSumServlet に送信します
  5. 結果ページ(/app/sutd/result)— サーバーセッションから結果を読み取ります

セットアップ

前提条件
  • Node.js 20.19+(Node 22 では 22.12+)
  • Telegram ボットトークン(@BotFather から取得)
  • 公開アクセス可能な HTTPS サーバー(Telegram WebApp ボタンに必要)
インストール
# Install backend dependencies
npm install

# Install and build the frontend
npm run build:frontend
環境変数

.env ファイルを作成します。

TELEGRAM_BOT_TOKEN=your_bot_token_here      # legacy single-bot token
NUS_TELEGRAM_BOT_TOKEN=your_nus_bot_token   # optional, for concurrent nusairconbot
SUTD_TELEGRAM_BOT_TOKEN=your_sutd_bot_token # optional, for concurrent sutdairconbot
PAYMENT_SESSION_SECRET=replace_with_openssl_rand_hex_32 # stable secret for signed payment/result tokens
SERVER_URL=https://your-public-server.example.com
OWNER_CHAT_ID=your_telegram_chat_id   # receives feedback notifications
MAINTENANCE_MODE=false                # set to "true" to put the app and bot in global maintenance mode
MAINTENANCE_MESSAGE=                  # optional custom global maintenance message
OWNER_STATUS_TIMEOUT_MS=4000          # optional timeout for owner /status EVS/eNETS checks
TOPUP_DISABLED=false                  # set to "true" to show maintenance message
DB_DIR=.                              # local SQLite dir; use /data on Railway with a mounted volume
TELEGRAM_BOT_MODE=                    # production/Railway defaults to webhook; dev defaults to polling
TELEGRAM_BOT_AUDIENCE=nus             # legacy single-bot mode: "nus" or "sutd"
LOW_BALANCE_CRON_SECRET=              # optional, enables the low-balance check endpoint
MCP_ALLOWED_ORIGINS=                  # optional, comma-separated extra browser origins allowed to call /mcp
GOOGLE_SITE_VERIFICATION_FILE=        # optional Search Console HTML file name, e.g. googleabc123.html
GOOGLE_SITE_VERIFICATION_CONTENT=     # optional exact file body if Google provides non-default content

Telegram WebApp の決済ボタンを機能させるには、SERVER_URL を HTTPS にする必要があります。HTTP の場合、ボットは通常のブラウザーリンクに切り替わります。Railway では https://${{RAILWAY_PUBLIC_DOMAIN}} に設定するか、未設定のままにしてください。アプリが RAILWAY_PUBLIC_DOMAIN から値を取得します。

PAYMENT_SESSION_SECRET には、openssl rand -hex 32 などで生成した、変更されないランダムな値を設定してください。省略した場合、アプリは設定済みのボットトークンを代わりに使用します。ボットトークンが変更されない限り、Railway のスリープ後も有効です。

Google Search Console の URL プレフィックス確認には、HTML ファイル方式を使用し、ダウンロードしたファイル名を GOOGLE_SITE_VERIFICATION_FILE に設定してください。既定のレスポンス本文は google-site-verification: <file name> です。Google からダウンロードしたファイルの内容が異なる場合は、ファイル本文と完全に同じ値を GOOGLE_SITE_VERIFICATION_CONTENT に設定してください。

本番環境、または RAILWAY_PUBLIC_DOMAIN が設定されている場合、Railway Serverless が Telegram からの受信リクエストに応じてスリープと復帰を行えるよう、設定済みの各ボットは既定で Telegram Webhook を使用します。常時稼働するデプロイの場合に限り、TELEGRAM_BOT_MODE=polling を設定してください。シングルボットモードで使用できるオプションの Webhook 変数は、TELEGRAM_WEBHOOK_PATH、TELEGRAM_WEBHOOK_SECRET、TELEGRAM_DROP_PENDING_UPDATES です。複数のボットを同時に稼働させる場合、カスタムパスが必要な場合に限り、NUS_TELEGRAM_WEBHOOK_PATH、NUS_TELEGRAM_WEBHOOK_SECRET、SUTD_TELEGRAM_WEBHOOK_PATH、SUTD_TELEGRAM_WEBHOOK_SECRET を使用してください。それ以外の場合は、一意のパスが自動生成されます。

1 つのデプロイで複数の Telegram ボットを同時に稼働させるには、NUS_TELEGRAM_BOT_TOKEN と SUTD_TELEGRAM_BOT_TOKEN の両方を設定してください。同じサービスで nusairconbot と sutdairconbot が起動し、それぞれ別のコマンドメニュー、Webhook パス、メモリー内セッション、決済通知を使用します。シングルボットのデプロイでは、引き続き TELEGRAM_BOT_TOKEN と TELEGRAM_BOT_AUDIENCE を使用できます。

実行
# Development (two terminals)
npm run dev            # Express backend on :3000
npm run dev:frontend   # Vite dev server on :5173

# Production
npm run build:frontend
npm start

本番環境では、Express がフロントエンドを /app/ で配信します。開発環境では、Vite が /webapp、/cp2nus、/sutd をバックエンドにプロキシします。

ローカルで開発する場合は、DB_DIR=. を設定するか、省略してください。Railway では、/data にマウントしたボリュームをアタッチしてから、DB_DIR=/data を設定してください。

Railway Serverless

このリポジトリには、本番ビルド、起動コマンド、/health チェックを設定した railway.json が含まれています。Railway Serverless 自体は、ダッシュボードで有効にします。

  1. サービスにパブリックドメインを追加します。
  2. 1 つのボットを使用する場合は TELEGRAM_BOT_TOKEN を、複数のボットを同時に稼働させる場合は NUS_TELEGRAM_BOT_TOKEN と SUTD_TELEGRAM_BOT_TOKEN を設定します。また、PAYMENT_SESSION_SECRET、SERVER_URL=https://${{RAILWAY_PUBLIC_DOMAIN}}、および OWNER_CHAT_ID などの任意の変数も設定します。
  3. /data にマウントしたボリュームをアタッチし、保存済みユーザー情報が再起動後も保持されるように DB_DIR=/data を設定します。
  4. サービス設定 > Deploy > Serverless に移動し、Serverless を有効にします。
  5. デプロイします。起動時に Telegram Webhook が自動登録されます。

コールドスタートにより、最初の Telegram リクエストや Web リクエストの応答が遅くなることがあります。また、サービスの復帰中に、最初のリクエストで Railway が 502 を返す場合があります。ボットのセッションとオーナーへの返信スレッドはメモリー内に保存され、再起動やスリープによってリセットされます。決済トークンと結果トークンは PAYMENT_SESSION_SECRET で暗号化されており、有効期限が切れるまでは再起動後も読み取れます。保存済みのメーター ID は SQLite に保持されます。

テスト
# Backend tests
npm test

# Frontend tests
cd frontend && npm test

# Lint backend and frontend
npm run lint

独自の Claude(MCP)を接続する

このサービスは、公開読み取り専用の MCP サーバーを POST /mcp で公開しているため、ユーザーは独自の AI アシスタントを接続し、その動作を自分で決められます。アカウントも API キーも必要ありません。

ツール機能
get_meter_balance現在の残高(NUS と SUTD)
get_meter_usage過去 7 日間の使用量と分析(NUS のみ)
get_recent_topups最近のチャージ履歴(最大 10 件、NUS と SUTD)
get_topup_linkチャージページへのリンク。ユーザー自身がカードで支払います

ツールは支払いを実行できず、カード情報を受け取ることもありません。応答はユーザーの AI プロバイダーに渡されるため、部屋の住所は含まれません。

接続するには、Claude の設定で https://<your-domain>/mcp をカスタムコネクターとして追加します。Claude Code では、次を実行します。

claude mcp add --transport http evs-meter https://<your-domain>/mcp

各ユーザーは、たとえば Claude プロジェクトなどで、アシスタントへの指示を自分で設定します。

My NUS meter ID is 12345678. When I ask about aircon credit, check the balance
and work out how many days it will last from my recent usage. If it will last
under 3 days, suggest a top-up amount and give me the top-up link.

サーバーはステートレスなので、Railway Serverless で動作します。ツール呼び出しは IP アドレスごとに 5 分間で 120 回までに制限されており、バッチ送信はできません。ホスト型クライアントは IP アドレスを共有するため、この制限はウェブサイトの照会制限より高く設定されています。

ツールエラーは JSON 形式なので、エージェントは次に何をするか判断できます。

{ "code": "METER_NOT_FOUND", "message": "EVS has no NUS meter with this ID. ...", "retryable": false }
コード再試行可能意味
METER_NOT_FOUNDいいえEVS がそのシステムのメーターを認識できません
INVALID_AMOUNTいいえ金額がシステムのチャージ上限または下限の範囲外です
TOPUPS_PAUSEDいいえ現在、チャージは停止されています
EVS_UNAVAILABLEはいEVS に接続できなかったか、詳細情報が返されませんでした
RATE_LIMITEDはいこの IP アドレスからの呼び出しが多すぎます。retryAfterSeconds を含みます

get_topup_link は最初にメーターを確認します。EVS に接続できない場合も、meterVerified: false を含むリンクを返します。

すべてのツールは出力スキーマを宣言し、structuredContent を返します。また、これを読み取らないクライアント向けに、同じ JSON をテキストとして返します。

エージェントに毎日残高を確認させ、残高が少なくなったときに支払いリンクを送信させるには、残高の定期確認を参照してください。独自のコピーを実行する場合や、その上に独自の自動化を構築する場合は、セルフホスティングガイドを参照してください。

ボットコマンド

コマンド説明
/startメインメニューを表示する
/topup電気料金のチャージを開始する
/balanceメーター残高を確認する
/usage過去 7 日間の使用量を表示する
/topups最近のチャージ履歴を表示する
/alerts残高低下アラートを表示または変更する
/feedbackフィードバックを送信するか、問題を報告する
/cancel現在の操作をキャンセルする
/helpヘルプと対応システムを表示する
/statusオーナー専用のサービス稼働状況を表示する
/statsオーナー専用の実行時スナップショットを表示する

残高不足アラート

/alerts on を実行すると、ユーザーはオプトインします。対象となるのは、そのボットで保存済みのメーターです。ボットでトップアップを開始すると、メーターが保存されます。オプトイン済みのメーターは1日1回確認され、残高が SGD 5 未満の場合、そのチャットにメッセージが届きます。メッセージにはボットの通常のトップアップフローを開始するボタンがあり、ユーザーは引き続き金額を選択してカードで支払います。

メーターごとに、残高不足の状態が続く期間中にアラートが送信されるのは1回だけです。確認時に残高が SGD 5 以上に戻っていれば、次回は再びアラートが送信されます。残高の取得に失敗した場合はスキップされ、残高がゼロとして扱われることはありません。ユーザーがボットをブロックすると、アラートは停止します。また、/forget を実行すると、メーターとともにアラートも削除されます。

毎日の確認を有効にするには、次の手順に従います。

  1. openssl rand -hex 32 でシークレットを生成します。
  2. Railway で LOW_BALANCE_CRON_SECRET として設定します。
  3. 同じ値を、同じ名前の GitHub リポジトリシークレットとして追加します。

.github/workflows/low-balance-check.yml は、シンガポール時間の 09:00 に POST /internal/low-balance-check を呼び出します。この処理により、Railway Serverless がサービスをスリープ状態にしていた場合もサービスが起動します。テストするには、GitHub Actions タブからワークフローを手動で実行します。GitHub のスケジュール実行は遅れることがあり、リポジトリに60日間アクティビティがない場合は無効になります。

ボットのセッションフロー

セッションはメモリー内に 15-minute TTL で保存されます。競合状態を防ぐため、特定のチャットに対するすべてのメッセージは、そのチャット専用のロックを通じて順番に処理されます。サーバーレス環境へのデプロイでは、Railway がサービスを停止し、その後再起動するとセッションがリセットされます。トップアップフローのステージは次のとおりです。

idle
  → awaiting_meter_id          (8-digit ID; prefetches balance + 7-day usage;
                                NUS always uses cp2nus, no hostel picker)
  → awaiting_amount            ($6–$50 SGD)
  → awaiting_payment           (WebApp Pay button; re-prompts on text)
  → idle                       (reset after WebApp closes)

  → awaiting_meter_id_balance  (/balance with no saved meter)
  → awaiting_meter_id_usage    (/usage with no saved meter)
  → awaiting_meter_id_topups   (/topups with no saved meter)

  → awaiting_feedback_rating   (/feedback — star rating keyboard)
  → awaiting_feedback_text     (free-text or ⏭ Skip)
  → idle

/balance、/usage、/topups は1ステップのステージを使用し、1回応答するとアイドル状態に戻ります。

決済セッション

決済セッション(/webapp/bootstrap で作成)は、10-minute TTL の暗号化シール付きトークンで、ボットのセッションとは別に管理されます。このトークンには、決済に必要なメーター ID、金額、住所、残高、eNETS ゲートウェイのフィールドが含まれます。これにより、Railway Free が新しいプロセスでサービスを起動した際に、進行中の決済セッションが失われるのを防ぎます。

/webapp/enets_pay で最終結果が確定すると、サーバーはすぐに Telegram の決済通知を送信し、24-hour TTL のシール付き結果トークンを返します。決済トークンと結果トークンには内部の tokenKind が設定されています。決済ルートは結果トークンを拒否し、結果ルートは決済トークンを拒否します。React の結果ページは GET /webapp/session?token= から結果データを取得します。決済結果にクエリパラメーターが使用されることはありません。CP2NUS の領収書 PDF は引き続き揮発性のメモリー内キャッシュに保持されるため、結果ページに領収書リンクが表示されるのは、そのキャッシュエントリーが利用可能な場合のみです。

ユーザーストア

保存済みのメーター ID とホステルは、better-sqlite3 を使用して SQLite データベース(evs_users.db)に永続化されます。データベースは DB_DIR に書き込まれます(Railway Volume が存在する場合は /data、それ以外の場合はプロジェクトのルートディレクトリ)。ボットのセッションや決済セッションとは異なり、ユーザーストアは再起動後も保持されます。

オーナー返信のスレッド管理

ユーザーがフィードバックを送信すると、ボットは OWNER_CHAT_ID に通知を転送します。オーナーはその Telegram 通知に直接返信でき、ボットが返信をユーザーに転送します。返信スレッドは、7-day TTL のメモリー内 pendingReplies マップで追跡されます。

注:スレッド管理の対象は、元の通知メッセージのみです。オーナーが自分の返信に返信しても、そのメッセージがユーザーに自動転送されることはありません。転送された元の通知への返信のみが捕捉されます。

プロジェクト構成

├── server.js                        # Express entry point; serves React at /app/
├── routes/
│   ├── cp2.js                    # WebApp + API routes for legacy cp2 (hidden from UI)
│   ├── cp2nus.js                 # WebApp + API routes for cp2nus
│   ├── sutd.js                   # WebApp + API routes for SUTD
│   ├── mcp.js                    # Read-only MCP server for AI assistants
│   ├── alerts.js                 # Secret-protected trigger for the low-balance check
│   └── website.js                # Standalone website lookup + feedback API
├── services/
│   ├── cp2Service.js             # Legacy cp2 purchase flow: EVS WebPOS scraping + eNETS proxy
│   ├── cp2nusService.js          # Purchase flow: EVS JSON API + eNETS PP + NETS API
│   ├── sutdService.js            # SUTD balance, history, and WebPOS purchase flow
│   ├── ore.js                    # ORE API: meter summary, usage history, top-up history
│   ├── meterLookup.js            # Balance, usage, and top-up lookups shared by the website and MCP
│   ├── paymentSession.js         # Sealed payment/result tokens and receipt cache
│   ├── paymentNotification.js    # Telegram payment result notification helper
│   ├── paymentSubmitLock.js      # Short-lived duplicate payment submit guard
│   ├── utils.js                  # HTML parsing, result normalisation, XSS escaping
│   ├── validators.js             # Meter ID validation and strict amount parsing
│   ├── config.js                 # Base URLs and shared HTTP headers
│   └── analytics.js              # Event tracking and exception capture
├── bot/
│   ├── index.js                  # Telegraf handlers + webhook/polling runtime
│   ├── handlers/                 # Command and text message handlers
│   ├── services/                 # Bot session, user store, lookup helpers
│   └── constants.js              # Stage names, keyboards, shared messages
├── views/
│   └── errorPage.js              # Shared HTML error page for Express error responses
└── frontend/                     # React + Vite frontend
    ├── src/
    │   ├── App.jsx               # React Router routes
    │   ├── pages/
    │   │   ├── HomePage.jsx      # System selection + meter ID + amount entry
    │   │   ├── LoadingPage.jsx   # Spinner; calls /webapp/bootstrap
    │   │   ├── CardPaymentPage.jsx  # RSA card form; calls /webapp/enets_pay
    │   │   ├── ResultPage.jsx    # Payment outcome
    │   │   └── TermsPage.jsx     # Terms of Use
    │   ├── components/           # Card, DetailRow, Logo, ErrorCard
    │   └── lib/                  # rsa.js, cardBrand.js, validation.js
    └── __tests__/                # Vitest + Testing Library tests

注意事項

  • Bot セッションはメモリ上に保存され、15 分後に期限切れになります。決済トークンは封印されており、PAYMENT_SESSION_SECRET が安定していれば再起動後も有効です。保留中の決済トークンは 10 分後、完了結果トークンは 24 時間後に期限切れになります。ユーザーストア(保存済みメーター ID)は SQLite によって管理され、再起動後も保持されます。
  • /webapp/enets_pay は、加盟店取引参照番号をキーとするプロセス内の送信ロックを使用し、処理中の決済送信の重複を HTTP 409 で拒否します。
  • カード情報はサーバーに送信する前に、ブラウザー内で RSA 暗号化されます。サーバーが平文のカード番号や CVV を見ることはありません。
  • cp2nus フローでは、トップレベルの netsMid(UMID_xxx)と paymtNetsMid(paymtSvcInfoList[0] のアクワイアリング MID)を区別します。間違った MID を使うと、決済はエラーを表示せずに失敗します。
  • ORE(ore.evs.com.sg)と eNETS の初期化エンドポイント(p-1.evs.com.sg)は、現在、空の Authorization: Bearer ヘッダーを付けて呼び出されており、正常に動作します。トークンは不要です。EVS2 ポータルには、短期間(約 1 時間)有効な JWT を発行するユーザー認証サービス(evs2u.evs.com.sg/login)もあるため、将来的に EVS がこれらのエンドポイントで実際のトークンを必須にする可能性があります。照会やチャージが 401/403 を返すようになった場合、これが原因と考えられ、その際は JWT を取得して付与するようフローを変更する必要があります。
  • 受け付けるチャージ金額の入力形式は厳密です。数字のみ、または $10 や S$ 10.00 など一般的な通貨記号を付けた形式を受け付けます。形式が混在した不正な文字列は拒否されます。NUS の最低チャージ額は $6.00 SGD、SUTD の最低チャージ額は $10.00 SGD で、現在、すべてのシステムでチャージ額の上限は $50.00 SGD です。
  • Web サイトのエントリーポイント(/app/)と Telegram Mini App は、同じ Express ルートと React ページを使用します。別々のコードパスはありません。
  • グローバルメンテナンスモードは、/maintenanceon(オーナーコマンド)で実行時に有効にするか、起動時に MAINTENANCE_MODE=true を設定して有効にできます。アプリと Bot 全体でグローバルメンテナンスメッセージがユーザーに表示されます。サービスへの接続を維持し、オーナーが /maintenanceoff を実行できるよう、/health と Telegram Webhook は引き続き利用できます。メッセージを変更するには MAINTENANCE_MESSAGE を設定してください。従来の別名として GLOBAL_MAINTENANCE=true も使用できます。
  • チャージは、/topupoff(オーナーコマンド)で実行時に無効にするか、起動時に TOPUP_DISABLED=true を設定して無効にできます。このフラグが設定されたときにチャージセッション中の Bot ユーザーは、セッションがリセットされ、メンテナンスメッセージが表示されます。Web サイトの決済開始、読み込み、カード、ブートストラップ、カード送信の各ルートも、チャージが無効な間はメンテナンス応答を返します。照会、利用規約、結果、フィードバックの各ルートは引き続き利用できます。

ライセンス

MIT。これは非公式のツールであり、NUS、SUTD、EVS、eNETS とは提携していません。

コメント

MCP は初めてですか?

プロトコルの概要から最初のサーバーの導入まで、よくある疑問にまとめて答えます。

AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。