EVS Meter Tools について
EVS Meter Tools は、対応する NUS と SUTD の電気メーター向けウェブアプリ兼 Telegram ボットです。残高、使用量、チャージ履歴を確認でき、対応する場合はカードでチャージできます。読み取り専用の MCP サーバーは、支払いやカード情報の処理なしで AI アシスタントにメーター情報と支払いリンクを提供します。
主な機能
- 01NUS と SUTD の残高確認
- 02使用量と最近のチャージ履歴
- 03クレジットカードのチャージリンク
- 04Telegram ボットとウェブアプリ
- 05読み取り専用のメーターツール
- 06MCP にカード情報を渡さない設計
活用シーン
- 01NUS または SUTD メーターの残高確認
- 02最近の使用量とチャージ履歴の確認
- 03対応メーターのチャージリンク取得
- 04AI アシスタントで残高不足を確認
MCP 対応
公開の読み取り専用 MCP サーバーは、残高、NUS の使用量、最近のチャージ履歴、チャージリンクを取得するツールを提供します。支払いの実行やカード情報の受信はできず、応答に部屋の住所を含めません。
設定
リモートサーバーはインストール不要です。クライアントを選んで設定をコピーするか、ワンクリックで導入できます。
https://nusaircon-production.up.railway.app/mcpclaude mcp add --transport http nicholasohjj-nusaircon https://nusaircon-production.up.railway.app/mcp{"mcpServers":{"nicholasohjj-nusaircon":{"type":"http","url":"https://nusaircon-production.up.railway.app/mcp"}}}Cursor で開く{"servers":{"nicholasohjj-nusaircon":{"type":"http","url":"https://nusaircon-production.up.railway.app/mcp"}}}VS Code で開く{"mcpServers":{"nicholasohjj-nusaircon":{"type":"http","url":"https://nusaircon-production.up.railway.app/mcp"}}}{"mcpServers":{"nicholasohjj-nusaircon":{"serverUrl":"https://nusaircon-production.up.railway.app/mcp"}}}コマンドはターミナルで実行し、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 |
| SUTD | SUTDMain / 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 のメーターにのみ使用されていました。
/webapp— ORE からメーターの住所と残高を取得し、住所と残高をクエリーパラメーターに含めて React の読み込みページにリダイレクトします- ブートストラップ(
/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 のカード入力ページにリダイレクトします
- カード入力ページ(
/app/pay)— React コンポーネント。/webapp/session経由でセッションを取得し、eNETS の RSA スクリプトを使ってクライアント側でcardNo + cvvを暗号化します - 支払いプロキシ(
/webapp/enets_pay):POST https://www.enets.sg/GW2/uCredit/pay- 優先経路: EVS のコールバックフォームを抽出 →
POST /EVSWebPOS/transSumServlet→parseEvsTransactionSummary - フォールバック:
parseEnetsResultで eNETS の領収書 HTML を直接スクレイピング - 結果(
status、merchantTxnRef、reason)をサーバー側セッションに書き戻します
- 結果ページ(
/app/result)— React コンポーネント。/webapp/session経由でサーバーセッションから結果を読み取ります
CP2NUS — NUS の主要なホステルフロー
EVS JSON API と eNETS Payment Page(enetspp)ホストを直接使用します。移行済みメーター向けの主要な NUS ホステルフローです。
/webapp— cp2 と同じです。メーター情報を取得し、/app/cp2nus/loadingにリダイレクトします- ブートストラップ(
/cp2nus/webapp/bootstrap)—runBootstrapを順に実行します:- メーターシステムの確認 —
isCp2Meter()ガードにより、WRONG_SYSTEMエラーで cp2 メーターを拒否します。ネットワーク障害時は確認をスキップし、フローを続行します init_pay—POST /enets/init_pay→{ txn_identifier, req, sign }meter_info—getMeterSummary→buildPayDisplayAddressenetspp_pay—buildEnetsPayUrl→GET enetspp/pay?p=…→txnReq、keyId、hmacを抽出しますTxnReqListener—POST /GW2/TxnReqListener→ RSA キー、netsTxnRef、netsMid、paymtNetsMid、txnRand、keyId、hmac
- メーターシステムの確認 —
- カードページ(
/app/cp2nus/pay)— RSA 暗号化は同じです。トップレベルのnetsMidではなく、paymtNetsMid(アクワイアリング MID)をpanSubmitFormで使用します - 決済プロキシ(
/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 デコードします
- 結果ページ(
/app/cp2nus/result)— サーバーセッションから結果を読み取ります
SUTD
SUTD EVS WebPOS ページと SUTD クレジット決済ブリッジを使用します。
/sutd/webapp— SUTD のメータークレジットを取得し、/app/sutd/loadingにリダイレクトします- ブートストラップ(
/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.jspPOST /enets2/PaymentListener.do→ RSA 公開鍵、netsMid、netsTxnRefを抽出します
- メーター ID を指定して
- カードページ(
/app/sutd/pay)— NUS の各フローと同じ RSA 暗号化カード入力です - 決済プロキシ(
/sutd/webapp/enets_pay)— 暗号化されたカードデータを eNETS に送信し、その後コールバックを/EVSSUTDWebPOS/transSumServletに送信します - 結果ページ(
/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 つのボットを使用する場合は
TELEGRAM_BOT_TOKENを、複数のボットを同時に稼働させる場合はNUS_TELEGRAM_BOT_TOKENとSUTD_TELEGRAM_BOT_TOKENを設定します。また、PAYMENT_SESSION_SECRET、SERVER_URL=https://${{RAILWAY_PUBLIC_DOMAIN}}、およびOWNER_CHAT_IDなどの任意の変数も設定します。 /dataにマウントしたボリュームをアタッチし、保存済みユーザー情報が再起動後も保持されるようにDB_DIR=/dataを設定します。- サービス設定 > Deploy > Serverless に移動し、Serverless を有効にします。
- デプロイします。起動時に 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 を実行すると、メーターとともにアラートも削除されます。
毎日の確認を有効にするには、次の手順に従います。
openssl rand -hex 32でシークレットを生成します。- Railway で
LOW_BALANCE_CRON_SECRETとして設定します。 - 同じ値を、同じ名前の 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 とは提携していません。
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。