fieldtwin について
fieldtwinはJobber、Housecall Pro、QuickBooks OnlineのAPIをローカルで再現し、連携開発者やテスト担当者、業務エージェントを開発するチーム向けに提供します。再現可能な業務データに加え、障害の制御、変更チェックポイント、巻き戻しに対応します。
主な機能
- 013種類の業務APIのリクエストと応答を再現
- 02システム間で共通する架空の業務データを生成
- 03スロットリング、障害、重複Webhookを発生
- 04シードデータでテストを再実行
- 05変更履歴の確認とチェックポイントへの巻き戻し
- 06時間を進めて期限切れやリトライをテスト
活用シーン
- 01顧客アカウントに触れずに連携をテスト
- 02CIでAPI障害を再現してリトライを検証
- 03エージェントが実際に行った業務データ変更を確認
- 04システム間のデータ不一致に対する同期処理をテスト
設定
ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。
npx -y fieldtwinFIELDTWIN_PORTThe port the twin listens on. The MCP server starts a twin of its own, so pick another port if one already runs on 4010.FIELDTWIN_HOSTThe address the twin listens on. Keep the loopback address unless something else must reach the twin.FIELDTWIN_AUTHシークレットany-token accepts any Bearer token; oauth accepts only tokens from the twin's OAuth endpoints.FIELDTWIN_SEEDWhich fake business to generate: the same seed builds the same world.ローカルサーバーにはマシン上の実行環境(Node.js 18 以上など)が必要です。設定ファイルの場所はクライアントごとに異なります。
ツール
14掲載時のツール一覧 · 1 日前に更新
このツールにパラメーターはありません。
詳細
fieldtwin
Jobber、Housecall Pro、QuickBooks Onlineをノートパソコン上で実行できます。実際のAPI形式、実在しそうな架空の事業者、そして必要に応じて発生させられるあらゆる障害を備えています。顧客のアカウントに触れることなく、連携機能やAIエージェントを構築・テストできます。
2分間のデモを見る:ダッシュボードでツインを操作し、エージェントによる変更を一覧表示して巻き戻し、同期によってずれた帳簿を整合させます。
コードの接続先を、api.getjobber.com や quickbooks.api.intuit.com の代わりに http://localhost:4010 に(または api.housecallpro.com の代わりに http://localhost:4010/housecallpro に)設定します。顧客、技術者、数か月分の作業、見積もり、請求書、支払いを含む架空の事業者を使い、すべてのシステムで同じパス、ヘッダー、JSONが返されます。次は、厳しい条件を試してみましょう。QuickBooksをスロットリングし、書き込みの応答を失わせ、Webhookを二重に配信し、すべてのトークンの有効期限を過ぎるまで時計を進めます。その後、コードが何を変更したかを正確に確認し、元に戻せます。
curl -X POST localhost:4010/_twin/checkpoints/before-agent # name this moment
# ...let your integration or agent loose on http://localhost:4010...
curl -s "localhost:4010/_twin/checkpoints/before-agent/changes?source=api" | jq -r '.[].summary'
2026-07-13T11:00:00.000Z quickbooks CustomerCreated Id=26 "Dana Whitfield" SyncToken=0 (api)
2026-07-13T11:00:00.000Z quickbooks InvoiceCreated Id=29 DocNumber=1001 "Dana Whitfield" TotalAmt=185.00 Balance=185.00 (api)
2026-07-13T11:00:00.000Z quickbooks CustomerUpdated Id=3 "Harper Alder Jr" SyncToken=1 (api)
curl -X POST localhost:4010/_twin/checkpoints/before-agent/rewind # and it never happened
実行のたびにシードから開始するため、同じリクエストにはバイト単位で同じ結果が返されます。一度遭遇したバグは、再び再現できます。
fieldtwin を選ぶ理由
フィールドサービスや専門工事の事業者向けソフトウェアを開発しているなら、選択肢はご存じでしょう。顧客の本番帳簿でテストして、問題が起きないことを祈る。ベンダーのサンドボックスへのアクセスを待ち、空のアカウントを手作業で埋める。あるいは、想定どおりの値だけを返すモックを作り、想定外の事態を見逃す。AI エージェントには、間違いを何度でも、並行して起こせる場所が必要ですが、どの方法もそれには適していません。
| 顧客の本番アカウント | ベンダーのサンドボックス | 手書きのモック | fieldtwin | |
|---|---|---|---|---|
| 安全に壊せる | いいえ | はい | はい | はい |
| すぐに使える | 顧客の信頼を得てから | 開発者アカウント、アプリ、セットアップの後 | 各エンドポイントを書いた後 | npx fieldtwin |
| 現実的なデータ | はい | 一般的なサンプル、またはなし | 自分で入力した内容 | 数か月分の履歴を持つ、シード済みの事業者 |
| Jobber、Housecall Pro、QuickBooksで同じ事業者を表現 | はい | いいえ | いいえ | はい。意図的にずれた状態にもできます |
| クリーンな状態に戻す | できません | 手作業 | 再起動 | POST /_twin/reset |
| 429、5xx、応答消失、Webhookの重複を必要に応じて発生 | いいえ | いいえ | 想定したもののみ | はい。ベンダー独自のエラー形式で発生します |
| 時間を先に進める(トークンの期限切れ、再試行、スケジュール済みイベント) | いいえ | いいえ | めったにできません | はい、すぐにできます |
| コードが変更した内容を確認して元に戻す | いいえ | いいえ | いいえ | チェックポイント、変更一覧、巻き戻し |
| オフライン、CI、複数コピーの並行実行 | いいえ | いいえ | はい | はい |
| 本番環境と完全に同じ | はい | ほぼ同じ | いいえ | いいえ。公開ドキュメントに基づいてモデル化されており、どの程度忠実かは FIDELITY.md に記載されています |
fieldtwinはベンダーのサンドボックスに取って代わるものではありません。サンドボックスで確認できない部分を補うものであり、本番稼働前にサンドボックスで最終確認する価値は依然としてあります。サンドボックスを利用できる場合は、fieldtwinの適合性テストスイートで実際のAPIの動作を記録し、ツインとの一致度を評価できます。
対象ユーザー
業務・現場サービス向けソフトウェアの連携機能を構築する開発者。 対応に苦労するバグは、意図的に再現できないものです。API のスロットリング、応答が返ってこない書き込み、二重に届く webhook、同期の途中で期限切れになるトークンなどです。fieldtwin では、いつでも好きなときに、すべてのシステムで現実味のある業務環境を使って、こうした状況を再現できます。さらに、twin time により待ち時間をスキップできます。連携の実例では、100 回を超えるリトライと 2 時間分のトークン期限切れを、わずか数秒で再現します。実行のたびにシードから開始するため、CI でもチームメイトのノートパソコンでも、障害をまったく同じように再現できます。より早くリリースでき、顧客の帳簿で悪夢のような事態が起きる前に、テストで問題を見つけられます。
企業の資金を扱う AI エージェントを構築するチーム。 エージェントが何度でも失敗できる場所と、実際に何をしたのかを示す証拠が必要です。チェックポイントを設定してエージェントに作業させ、エージェントが実行したと言う内容ではなく、実際に行った変更の正確な一覧に基づいて評価します。その後、巻き戻してもう一度実行できます。mismatched-books を読み込んでシステム間の不一致に気づくかを確認したり、flaky-apis を読み込んで諦めてしまうかを確認したりできます。ダッシュボードには、すべてがリアルタイムで表示されます。
業務向けサービスを販売する代理店やインテグレーター。 「お客様のアカウントに触れる前に、Jobber または Housecall Pro と QuickBooks の完全なシミュレーション環境でテストします」という約束を果たせます。慎重な事業主に導入を決断してもらう理由にもなります。
そして、実際に fieldtwin を実行することのない事業者自身。 顧客の重複登録や請求書の帳簿への未記録、支払いの二重記録を起こさない同期と、自社ではなく業務環境のコピーで失敗を経験した AI アシスタントを手にできます。
得られるもの
| 得られるもの | 代わりに必要なもの |
|---|---|
| Jobber、Housecall Pro、QuickBooks で同時に動作する、実際に使える模擬業務環境を 1 つのコマンドで起動 | 開発者アカウント、アプリのセットアップ、手入力のテストデータ |
| いつでも好きなときに発生させられるスロットリング、障害、応答の消失、webhook の重複 | 本番環境で初めて遭遇すること |
| 何時間分ものリトライ待ちやトークン期限切れを数秒で再現 | スリープを伴うテスト、またはその状況を一度も扱わないテスト |
| 同じシードなら、毎回同じ結果 | 「自分のマシンでは動く」 |
| コードやエージェントが行った変更の正確な一覧と、元に戻す手段 | ログからの推測や、二度とリセットできないアカウントでの作業 |
| オフラインで、ベンダーアカウントなしに、コミットのたびに実行できるテスト | リリース前にサンドボックスで手作業による確認 |
fieldtwin は Nick George が開発しています。デモ動画 2 本を掲載したページは nickgeorgeai.com/fieldtwin にあります。ほかの制作物や連絡先は nickgeorgeai.com をご覧ください。
モデル化されている内容
| QuickBooks Online | Jobber | Housecall Pro | |
|---|---|---|---|
| レコード | 顧客、品目、請求書、支払い | クライアント、作業、訪問、見積もり、請求書、ユーザー | 顧客とその住所、従業員、予約と明細項目を含む作業、見積もり、請求書 |
| 読み取り | ID による取得、クエリ言語(AND で連結する WHERE と =, <, >, LIKE, IN、ORDERBY、ページング、COUNT(*))、変更データキャプチャ | 双方向の Relay ページネーション、エンコードされた ID、日付指定の API バージョンに対応する GraphQL | page と page_size による REST リスト、並べ替え、顧客検索、ドキュメントに記載された作業および請求書のフィルター |
| 書き込み | SyncToken を使った顧客と請求書の作成・更新(古い場合は 5010)、部分更新または全項目更新、支払いの作成と適用、安全な再試行のための requestid | clientCreate | 顧客の作成・更新、住所の追加、作業の作成・スケジュール設定・手配 |
| レート制限 | 会社あたり毎分 500 リクエスト:HTTP 429 | クエリコストの上限:10,000 ポイント、毎秒 500 ポイント回復、THROTTLED | 公開されていません。障害ルールによって 429 が発生し、RateLimit-Reset が付与されます |
| Webhook | CloudEvents 形式、署名付き、Intuit のスケジュールに従って再試行 | HMAC 署名付き、少なくとも 1 回配信、再試行 | レコード全体を Api-Timestamp と Api-Signature で署名して送信し、再試行は 1 回 |
| OAuth 2.0 | 認可、トークン、更新(100 日間のローリング方式)、失効 | PKCE による認可、トークン、ローテーションされるリフレッシュトークン | 認可、トークン(30 日間)、ローテーションされるリフレッシュトークン、Authorization: Token としての API キー |
記載されていない機能は、架空のデータを返すのではなく、ベンダー独自のエラー形式で明確に失敗します。FIDELITY.md には、すべての動作、その一致度、参照元のドキュメントページが記載されています。
クイックスタート
Node 22 以降が必要です。
npx fieldtwin
fieldtwin: Rowntree Electric is open for business at http://localhost:4010
fieldtwin: auth is any-token: ANY Bearer token is accepted. Set FIELDTWIN_AUTH=oauth for real OAuth checks.
ブラウザーで http://localhost:4010 を開くと、すべての案内を確認できます。QuickBooks ツインから顧客を取得します(デフォルトでは任意の Bearer トークンを使用できます。認証を参照してください)。
curl -H "Authorization: Bearer dev" \
http://localhost:4010/v3/company/9341000000000001/customer/1
Jobber ツインからクライアントの一覧を取得します。
curl -X POST http://localhost:4010/api/graphql \
-H "Authorization: Bearer dev" \
-H "X-JOBBER-GRAPHQL-VERSION: 2025-04-16" \
-H "Content-Type: application/json" \
-d '{"query":"{ clients(first: 5) { nodes { id name } totalCount } }"}'
API キーを使って、Housecall Pro ツインからスケジュール済みの作業の一覧を取得します(任意のキーを使用できます)。
curl -H "Authorization: Token dev" \
"http://localhost:4010/housecallpro/jobs?work_status[]=scheduled&page_size=5"
代わりに、すぐに使える状況を設定して起動することもできます:npx fieldtwin flaky-apis(シナリオと障害を参照してください)。npx fieldtwin --help でシナリオと設定の一覧を確認できます。
fieldtwin 自体を開発する場合や、ダッシュボードと実例(Jobber から QuickBooks へ)を実行する場合は、代わりにクローンしてください:git clone https://github.com/nickgeorgeseo/fieldtwin && cd fieldtwin && npm ci && npm start。
コードでの使用
ベース URL だけを変更してください。パス、ヘッダー、リクエストボディの形式は実際の API と一致します。
| ベンダー | 実際のベース URL | ツインのベース URL |
|---|---|---|
| QuickBooks Online | https://quickbooks.api.intuit.com | http://localhost:4010 |
| Jobber | https://api.getjobber.com | http://localhost:4010 |
| Housecall Pro | https://api.housecallpro.com | http://localhost:4010/housecallpro |
QuickBooks ツインは、realm 9341000000000001 の 1 社を提供します。Housecall Pro ツインは /housecallpro 配下で実際のパスを提供します。一部のパス(/api/invoices/{uuid})は Jobber のパスと同じ形式で始まるためです。
QuickBooks と Jobber のツインは、ツインの時刻に基づいてベンダーが公開しているレート制限を適用します。Jobber はクエリのコストが残りの予算を超えると THROTTLED を返し、QuickBooks は 1 分間に 500 リクエストを超えると HTTP 429 を返します。時刻を進めると復旧し、リセットすると上限が満額に戻ります。Housecall Pro は制限を公開していないため、ツインがスロットリングするのは障害ルールで指定された場合のみです。どのツインにも、障害によって追加のスロットリングを発生させることができます。
設定
| 変数 | デフォルト | 機能 |
|---|---|---|
FIELDTWIN_PORT | 4010 | リッスンするポート |
FIELDTWIN_HOST | 127.0.0.1 | リッスンするアドレス。0.0.0.0 にすると、コンテナや別のマシンからアクセスできます。twin にアクセスできる人は誰でも操作できるため、信頼できるネットワーク上でのみ使用してください(SECURITY.md)。 |
FIELDTWIN_AUTH | any-token | oauth にすると、ベンダー API は実際の API と同じ方法でトークンをチェックします |
FIELDTWIN_SEED | 42 | 生成する架空のビジネスを指定します。同じシードからは常に同じビジネスが生成されます |
すべてのコミットでテストする
テストは専用の twin を起動し、アプリと同じ方法でアクセスします。各テストで新しい環境を読み込み、そこでコードを実行して変更リストを確認します。ログに記録された内容ではなく、コードが実際に行ったことを検証します。twin の時間は待ち時間を短縮するため、再試行やトークンの有効期限切れが数秒で発生します。また、各環境はシードから生成されるため、CI で失敗したテストは自分のノートパソコンでも同じように失敗します。
1. テストを書く。 このテストでは、応答を受け取れなかった場合に発生するバグを確認します。QuickBooks では同じ番号の請求書を 2 件受け付けるため、タイムアウト後に requestid なしで作成を再試行するコードは、同じ業務を二重に記帳してしまいます。
// test/sync.test.ts in your project, with fieldtwin installed: npm install --save-dev fieldtwin
import assert from "node:assert/strict";
import { after, beforeEach, test } from "node:test";
import { startTwin } from "fieldtwin";
import { syncPaidInvoices } from "../src/sync.ts"; // your code
const running = await startTwin(); // a twin of this file's own, on a free port
after(() => running.close());
/** Something the twin recorded: what changed, in which vendor, with the whole record as that vendor's API returns it. */
interface Change {
vendor: string;
type: string;
data: any;
}
/** A request to the twin's control plane, failing the test with the twin's reason if it refuses. */
async function twin(method: string, path: string) {
const response = await fetch(`${running.url}/_twin${path}`, { method });
if (!response.ok) throw new Error(`${method} /_twin${path}: ${await response.text()}`);
return response.json();
}
beforeEach(async () => {
await twin("POST", "/scenarios/flaky-apis"); // a fresh world for each test: 429s, 503s, and writes whose answer is lost
await twin("POST", "/checkpoints/before-sync");
});
test("books each invoice once, even when QuickBooks loses the answer to a write", async () => {
await syncPaidInvoices({ jobberUrl: running.url, quickbooksUrl: running.url });
const changes: Change[] = await twin("GET", "/checkpoints/before-sync/changes?source=api"); // what your code did
const booked = changes
.filter((change) => change.vendor === "quickbooks" && change.type === "InvoiceCreated")
.map((change) => change.data.invoice.DocNumber);
assert.ok(booked.length > 0, "the sync booked no invoices");
assert.equal(new Set(booked).size, booked.length, `an invoice was booked twice: ${booked.join(", ")}`);
});
Node の組み込みテストランナーを使用しており、Vitest でも同様に動作します。fieldtwin は ES モジュールなので、Jest ではESM モードが必要です。requestid なしで再試行する同期処理に対して実行すると、見つかった重複を示して失敗します。
✖ books each invoice once, even when QuickBooks loses the answer to a write
AssertionError [ERR_ASSERTION]: an invoice was booked twice: 41, 41
2. 実行する。 node --test(または npm test)を実行します。ほかに起動するものはありません。テストファイルが twin を起動して終了させます。Node は 22.18(および 23.6)から .ts のテストファイルをそのまま実行します。古い Node では node --test --experimental-strip-types を実行してください。そうしない場合、node --test はテストを検出できません。
3. すべてのコミットで実行する。 fieldtwin はほかの依存関係と同じように開発用依存関係なので、同じコマンドを CI で実行できます。テストが Node 上で動作しない場合や、テストの横で twin を 1 つ起動したままにしたい場合は、独立したステップとして起動し、応答があるまで待機してください。GitHub Actions の場合:
# .github/workflows/test.yml in your project
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
- name: Start the twin
run: |
nohup npx -y [email protected] > twin.log 2>&1 & # pin the version for repeatable runs
timeout 60 bash -c 'until curl -sf localhost:4010/_twin > /dev/null; do sleep 1; done'
- run: npm ci && npm test # your tests, with the twin at http://localhost:4010
- name: Keep the twin's event log
if: failure()
run: curl -s localhost:4010/_twin/log.jsonl > fieldtwin-log.jsonl
- if: failure()
uses: actions/upload-artifact@v7
with:
name: fieldtwin-log
path: fieldtwin-log.jsonl
実行が失敗した場合、そのイベントログは実行結果とともに保存されます。コードが twin に対して行ったすべての操作を読み取ったり、再生したりできます。
知っておくべき点:
- 同じ twin を共有するテストは、同じ環境を共有します。
startTwin()を呼び出す各ファイルにはそれぞれ専用の twin が割り当てられるため、ファイルは並列実行できます。Node のテストランナーと Vitest はデフォルトでファイル内のテストを順番に実行します。テストの横で起動した twin 1 つをすべてのテストで共有する場合は、ファイルを 1 つずつ実行してください(node --test --test-concurrency=1、Vitest の--no-file-parallelism、Jest の--runInBand)。 startTwin()にはコマンドと同じ設定を指定できます:seed、auth("any-token"または"oauth")、scenario(既成シナリオの名前、または YAML ファイルのパス)、port(指定がない場合は空いているポート)。戻り値にはurl、port、close()が含まれます。flaky-apisは OAuth トークンを実際にチェックします。そのため、本番環境と同様に、コードは twin の OAuth エンドポイントを通じて接続します。再試行だけをテストするには、シナリオの後にPUT /_twin/authと{"mode": "any-token"}を実行してください。- より厳密に確認するには、リファレンス統合の不安定性テストで
flaky-apisを通じて同期し、クリーンな実行と同じ状態で記帳が完了することを確認します。
実例:Jobber から QuickBooks への連携
examples/sync-jobber-invoice-to-quickbooks は、本番環境のコードに沿って書かれたリファレンス連携で、自由にコピーしてご利用いただけます。支払いが行われるたびに、スケジュールまたは Jobber の Webhook を通じて、支払い済みの Jobber の請求書をすべて QuickBooks に記帳します(顧客、請求書、支払い)。429 や 5xx に対するバックオフ、書き込み後に応答が失われた場合、Jobber のクエリコストのスロットリング、期限切れやローテーションする OAuth トークン、古い SyncToken、各システムで名前の表記が異なる顧客、二重または遅れて届く Webhook、同時に実行される 2 つの同期など、ツインが引き起こすあらゆる問題に対応します。
npm start # terminal 1
npm run example:sync # terminal 2
テストを見ると、ツインに対するテストの利点が分かります。flaky-apis の下でも、100 回を超えるリトライを経て、同じ同期処理が正常な実行とまったく同じ帳簿に落ち着きます。また、トークンの有効期限が切れるまで待つ 2 時間も、実際の時間はかかりません。
AI エージェントのテスト
ツインを使えば、エージェントが作業できるビジネス全体と、エージェントが行った操作の正確な記録が得られます。各テストにチェックポイントを設定し、エージェントに作業させたうえで、何をしたとエージェントが説明したかではなく、変更一覧に基づいて評価します。
curl -X POST localhost:4010/_twin/checkpoints/before-agent
# ...run your agent against the twin...
curl "localhost:4010/_twin/checkpoints/before-agent/changes?source=api"
curl -X POST localhost:4010/_twin/checkpoints/before-agent/rewind
API が反撃してきたときの対処を確認するには flaky-apis を、フィールドサービスシステムと QuickBooks の間の不一致に気づくかを確認するには mismatched-books を読み込んでください。コーディングエージェントにツイン自体を操作させるには、MCP サーバーを使用します。
認証
デフォルトでは、ツインはどのベアラートークンでも受け入れます。 これにより、クイックスタートは 1 つのコマンドで済みますが、OAuth コードをテストするには 適切な設定ではありません。ツインに実際の API と同じ方法でトークンを検証させるには、
FIELDTWIN_AUTH=oauth npx fieldtwinを指定して起動するか、シナリオにauth: oauthを追加するか、実行中のツインを次のコマンドで切り替えます。curl -X PUT localhost:4010/_twin/auth -H "Content-Type: application/json" -d '{"mode":"oauth"}'。
各ベンダーには、そのベンダー固有のパス(Housecall Pro の場合は /housecallpro 配下)にツインの OAuth 2.0 サーバーがあります。そのため、OAuth クライアントで変更が必要なのは URL だけです。
| エンドポイント | 実際の URL | ツインの URL |
|---|---|---|
| QuickBooks の認可 | https://appcenter.intuit.com/connect/oauth2 | http://localhost:4010/connect/oauth2 |
| QuickBooks のトークン | https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer | http://localhost:4010/oauth2/v1/tokens/bearer |
| QuickBooks の取り消し | https://developer.api.intuit.com/v2/oauth2/tokens/revoke | http://localhost:4010/v2/oauth2/tokens/revoke |
| Jobber の認可 | https://api.getjobber.com/api/oauth/authorize | http://localhost:4010/api/oauth/authorize |
| Jobber のトークン | https://api.getjobber.com/api/oauth/token | http://localhost:4010/api/oauth/token |
| Housecall Pro の認可 | https://pro.housecallpro.com/oauth/authorize | http://localhost:4010/housecallpro/oauth/authorize |
| Housecall Pro のトークン | https://api.housecallpro.com/oauth/token | http://localhost:4010/housecallpro/oauth/token |
ツインはすべての認可リクエストを即座に承認します(同意画面はありません)。また、任意の client_id と client_secret を受け入れます。その後はすべて、各ベンダーのドキュメントに従います。認可コードは一度だけ使用でき、アクセストークンの有効期間は 60 分(Housecall Pro は 30 日)です。Jobber と Housecall Pro はリフレッシュトークンをローテーションし、古いトークンを拒否します。QuickBooks のリフレッシュトークンは、ローリング方式で 100 日間有効です。トークンはツインの時刻で期限切れになるため、POST /_twin/clock/advance {"minutes": 60} を実行すれば、1 秒でトークンの更新処理をテストできます。(Jobber のアクセストークンは exp もツインの時刻に基づく JWT なので、自分の時計ではなく、ツインの時計と比較してください。)
Pro が独自に使用する Housecall Pro 連携では、代わりに API キーを使い、Authorization: Token <key> として送信します。ツインは Pro が作成したキーを判別できないため、どちらの認証モードでも任意のキーを受け入れます。
コマンドラインから Jobber のトークンを取得する方法:
CODE=$(curl -s -o /dev/null -w '%{redirect_url}' \
"http://localhost:4010/api/oauth/authorize?response_type=code&client_id=my-app&redirect_uri=http://localhost:3000/callback" \
| sed -E 's/.*code=([^&]+).*/\1/')
curl -s -X POST http://localhost:4010/api/oauth/token \
-d client_id=my-app -d client_secret=dev -d grant_type=authorization_code \
-d code=$CODE -d redirect_uri=http://localhost:3000/callback
Webhooks
各 Twin は、ベンダーと同じ方法で Webhook を送信します。各ベンダーはアプリごとに Webhook を設定します(開発者ポータル、または Housecall Pro の場合は Pro 自身の設定)。Twin では、これに相当するのがコントロールプレーン(または twin_set_webhooks MCP ツール)です。
curl -X PUT localhost:4010/_twin/webhooks -H "Content-Type: application/json" -d '[
{"vendor":"jobber","url":"http://localhost:3000/webhooks/jobber","topics":["INVOICE_UPDATE"],"clientId":"my-app","clientSecret":"dev"},
{"vendor":"quickbooks","url":"http://localhost:3000/webhooks/qbo","topics":["qbo.payment.created.v1"],"clientId":"my-app","clientSecret":"my-verifier-token"},
{"vendor":"housecallpro","url":"http://localhost:3000/webhooks/hcp","topics":["invoice.paid"],"clientId":"my-app","clientSecret":"my-signing-secret"}
]'
curl -X POST localhost:4010/_twin/actions/jobber/payInvoices -H "Content-Type: application/json" -d '2'
curl -X POST localhost:4010/_twin/actions/housecallpro/payInvoices -H "Content-Type: application/json" -d '2'
Jobber の配信には、Jobber が文書化しているペイロード({ "data": { "webHookEvent": { "topic", "appId", "accountId", "itemId", "occurredAt" } } })と、サブスクリプションのクライアントシークレットを使って生成された X-Jobber-Hmac-SHA256 署名が含まれるため、検証コードは変更せずに実行できます。Jobber と同様に、Twin は少なくとも 1 回は配信します。請求書の支払い時には INVOICE_UPDATE が 2 回発生し、アプリが 1 秒以内に受け付けなかった配信は、Twin 時間で後から再送されます。現時点では CLIENT_CREATE と INVOICE_UPDATE がモデル化されています。
QuickBooks の配信は、Intuit が現在送信している形式である CloudEvents です。たとえば、{ "specversion": "1.0", "type": "qbo.invoice.updated.v1", "intuitentityid": "130", "intuitaccountid": "9341000000000001", ... } のようなイベントの JSON 配列です。署名は intuit-signature に含まれ、サブスクリプションの clientSecret がアプリの検証トークンの代わりに使用されます。API を通じて作成または更新された顧客、請求書、支払いが通知され(qbo.customer.created.v1、qbo.customer.updated.v1、qbo.invoice.created.v1、qbo.invoice.updated.v1、qbo.payment.created.v1)、支払い時には、その支払いが充当される各請求書も通知されます。アプリは HTTP 200 で応答するまで 3 秒の猶予があります。応答しない場合、Twin は Intuit のスケジュールに従って、Twin 時間で 10 秒後から最長 6 時間ごとに再試行します。
Housecall Pro の配信には、文書化されている共通フィールド(event、company_id、event_occurred_at、event_created_at)と、イベント発生時点のレコード全体({ "event": "job.created", ..., "job": { ... } })が含まれます。Api-Timestamp と Api-Signature はドキュメントに記載された検証手順に従います。つまり、タイムスタンプ、ピリオド、ペイロードを連結した文字列を、サブスクリプションの clientSecret を署名シークレットとして HMAC-SHA256 で署名します。現時点では customer.created、customer.updated、job.created、job.scheduled、job.completed、job.paid、invoice.created、invoice.sent、invoice.payment.succeeded、invoice.paid がモデル化されています。ジョブの完了、請求書発行、支払いは、シナリオのタイムラインまたはコントロールプレーンから completeJobs、invoiceJobs、payInvoices アクションで実行します。ジョブの請求書を発行すると invoice.created、続いて invoice.sent が送信され、請求書に支払いを行うと invoice.payment.succeeded、invoice.paid、job.paid の順に送信されます。ドキュメントでは再試行が保証されていないため、Twin は各配信を 1 回だけ試みます。
各 URL への配信は、順番に 1 件ずつ行われます。応答が遅いアプリが、別の URL への配信を遅らせることはありません。GET /_twin/webhooks では、最近のすべての配信と、アプリの応答を確認できます。
webhook トピック(INVOICE_UPDATE、qbo.invoice.updated.v1、job.created、または "*")を指定した障害ルールは、リクエストではなく配信に影響します。配信が 2 回送信されたり、後続の配信が先に届くほど遅延したりします。障害が発生した配信には X-Twin-Fault が含まれます。
faults:
- name: jobber-webhooks-twice
vendor: jobber
webhook: INVOICE_UPDATE
rate: 0.3
effect: { duplicate: true }
- name: jobber-webhooks-late
vendor: jobber
webhook: "*"
rate: 0.2
effect: { delayMs: 300000 } # five minutes of twin time late
シナリオと障害
シナリオは、シード値、開始時刻、ワールドサイズ、障害、時間の経過に伴う動作を記述した YAML ファイルです。名前を指定して既成のシナリオでツインを起動することも、自分で用意したファイルで起動することもできます。
npx fieldtwin heat-wave-monday
npx fieldtwin my-scenario.yaml
| シナリオ | テスト内容 |
|---|---|
heat-wave-monday | HVAC が多忙な月曜日:顧客 120 人、QuickBooks への書き込みが制限され、Jobber の応答が遅く、新しいクライアントから午前中ずっと電話がかかってくる |
mismatched-books | フィールドサービスシステム(Jobber と Housecall Pro)と QuickBooks の内容が一致しません。顧客名の表記が異なる(メールアドレスは同じ)顧客、フィールドサービス側にしかおらず作業が同期されていない顧客、QuickBooks にしかいない顧客、QuickBooks では非アクティブとして登録されているものの、その後新しい作業の料金を支払った顧客がいます |
flaky-apis | 再試行のテスト:実際の OAuth チェックに加え、複数の障害を重ねます(QuickBooks の 429、503、応答が失われる書き込み、Jobber の 429、502、2 回または遅れて送信される Webhook、Housecall Pro の RateLimit-Reset を伴う 429、502、応答が失われる書き込み、遅れて送信される Webhook)。その間も顧客は支払いを続けます |
障害ルールは、ベンダー、メソッド、パス(pathIncludes、およびベンダーの OAuth エンドポイントなどを除外するための pathExcludes)でリクエストに一致し、指定した割合で発生します。
faults:
- name: qbo-throttled-writes
vendor: quickbooks
method: POST
rate: 0.2 # one in five matching requests
effect: { status: 429, message: "ThrottleExceeded" }
注入された障害はベンダー独自のエラー形式で返され、ベンダーが送信するヘッダーも含まれます(Housecall Pro の 429 には、ツインの時刻での RateLimit-Reset が含まれます)。また、X-Twin-Fault ヘッダーが付くため、実際のバグと区別できます。effect ではリクエストを遅延させる(latencyMs)ことも、応答を失わせることもできます。lostResponse: true にすると、ツインは先に処理を行い、その後エラーを返します。これは、ゲートウェイのタイムアウトによって成功した書き込みが隠れてしまう状態を再現します。これは再試行した書き込みが二重に実行されずに耐えなければならない障害です(QuickBooks の場合は requestid を送信します)。
- name: qbo-lost-answers
vendor: quickbooks
method: POST
rate: 0.1
effect: { status: 504, lostResponse: true }
シナリオでは、ツインの時刻の経過に応じて発生する動作もスケジュールできます。時計を進めると、各エントリーがそれぞれの時刻に実行され、ソースは scenario として記録されます。
timeline:
- at: "08:00" # business time; also "+90m", "+2h", or "2026-07-13T15:00:00Z"
jobber: { newClients: 12 } # new clients call in
- at: "13:00"
jobber: { payInvoices: 4 } # the longest-open invoices get paid
- at: "16:00"
housecallpro: { completeJobs: 2, invoiceJobs: 2, payInvoices: 1 } # techs finish work, the Pro invoices it, a customer pays
Housecall Pro にも、Jobber の newClients と同様の newCustomers があります。ジョブはすべての訪問が開始されて初めて完了となるため、completeJobs が予定時刻より前に作業を完了扱いにすることはありません。ジョブを完了しても請求書は作成されません。invoiceJobs は完了した作業のうち、完了から最も時間が経過したものから請求書を作成し、その後は payInvoices で支払いを行えます。
タイプミス、不明なキー、または UTC オフセットのない時刻が含まれるシナリオは、理由とともに読み込み時に拒否され、ツインの状態は変更されません。
コントロールプレーン
/_twin 配下のすべての機能は、ベンダーを模倣するのではなく、フェイクの世界を操作します。
| リクエスト | 処理内容 |
|---|---|
GET /_twin | 会社、シード、読み込まれたシナリオ、ツインの時刻、イベント数、認証モード、有効な障害、Webhook サブスクリプション、チェックポイント、次に予定されている内容 |
POST /_twin/reset | 最初の状態に戻します。シードされた世界(読み込まれている場合はシナリオの世界)と、そのタイムラインおよび障害に戻ります |
POST /_twin/clock/advance {"minutes": 60} | 時刻を進めます(レート制限が回復し、トークンが期限切れになり、予定されたイベントとその Webhook が発生します) |
POST /_twin/actions/:vendor/:action (body: params) | ベンダー独自のアプリで実行したかのように、その場で処理を実行します。例:jobber/payInvoices に 2、jobber/newClients に 3、housecallpro/invoiceJobs に 1 |
POST /_twin/checkpoints/:name | この時点に名前を付けます |
GET /_twin/checkpoints/:name/changes?source=api | その時点以降にコードが変更した内容を返します(seed、scenario、control を指定することもできます。source を指定しない場合はすべてが対象です)。各イベントには1行の summary が付いています。ログと同様に、after= を指定すると、前回の読み取り以降の内容を取得できます |
POST /_twin/checkpoints/:name/rewind | チェックポイント以降のすべての変更を元に戻します。同じリクエストには、同じ障害が再び発生します |
PUT /_twin/faults | 有効な障害ルールを置き換えます |
PUT /_twin/auth {"mode": "oauth"} | ベアラートークンを実際に検証するか(oauth)、任意のトークンを受け入れます(any-token) |
GET /_twin/scenarios | すぐに使えるシナリオと、それぞれのテスト内容を返します |
POST /_twin/scenarios/:name | すぐに使えるシナリオを読み込みます |
POST /_twin/scenario (YAML body) | 独自のシナリオを読み込みます |
GET /_twin/log.jsonl?since=:checkpoint | イベントログを1行につき1イベントで返します(since を指定しない場合はすべて)。シードと合わせれば、バグ報告に必要な情報がすべて含まれます。X-Twin-Log-Position ヘッダーにはログの終端位置が示されます |
GET /_twin/log.jsonl?after=:position | 前回の読み取りで取得した位置(またはイベントの seq)以降に記録された内容だけを返します。ログの増加を追う方法です。その間にログが変更された場合(リセット、巻き戻し、シナリオの読み込み、ツインの再起動)は 409 が返されます。その場合は最初から再読み込みしてください |
PUT /_twin/webhooks | Webhook サブスクリプションを置き換えます(Webhooks を参照) |
GET /_twin/webhooks | サブスクリプションと、それぞれの最近の配信に対するアプリの応答を返します |
ダッシュボード
dashboard/ は、実行中のツイン用の Web ダッシュボードです。ブラウザーからコントロールプレーンのすべての機能を使えるほか、イベントログの増加を確認できます。また、QuickBooks と Jobber または Housecall Pro を並べて表示し、各顧客と請求書が一致しているか、差異があるかを確認できます。
npm start # terminal 1: the twin
cd dashboard && npm ci && npm run dev # terminal 2: http://localhost:4020
これは独立した Next.js パッケージであり、ほかのクライアントと同様にツインを利用します。/_twin のみを使用するため、コードを監視してもレート制限を消費せず、障害も発生しません。
コーディングエージェントから使用する(MCP)
npx fieldtwin mcp は、stdio 経由で twin と MCP サーバーを起動します(クローンでは npm run --silent mcp を実行するため、npm 自身の出力は stdout に表示されません)。ツールはコントロールプレーンを反映しています。
| ツール | 内容 |
|---|---|
twin_status | 会社、シード、シナリオ、twin の時刻、認証モード、アクティブな障害、チェックポイント、次に予定されている内容 |
twin_reset | 読み込まれたシナリオの開始時点(またはデフォルトのワールド)に戻します |
twin_advance_clock | 時刻を進めます。進めている間に、予定されたシナリオイベントが発生します |
twin_checkpoint、twin_changes、twin_rewind | ある時点を記録し、それ以降の変更(必要に応じて自身の API 呼び出しのみ)を一覧表示して、取り消します |
twin_list_scenarios、twin_load_scenario | 名前で既製のシナリオを選ぶか、独自の YAML を読み込みます |
twin_set_faults | API を意図的に誤動作させます |
twin_set_auth | 任意のトークンを受け入れるか、OAuth トークンを実際に検証します |
twin_export_log | イベントログを JSON Lines 形式で、すべて、またはチェックポイント以降の分を出力します |
twin_run_action | 顧客の支払い、クライアントからの着信、技術者の作業完了、完了した作業の請求など、今すぐ何かを実行します |
twin_set_webhooks、twin_webhooks | アプリを Webhook に登録し、配信された内容とその応答を確認します |
各ツールには MCP のツールアノテーションが付いているため、変更を伴うツールを実行する前に確認するクライアントは、どのツールが該当するか把握できます。参照のみを行う5つのツールには読み取り専用のマークが付いています。既存の内容を置き換えたり破棄したりするツールには、破壊的操作のマークが付いています。対象は、リセット、巻き戻し、シナリオの読み込み、障害または Webhook の設定、チェックポイント名の再利用です。twin の外部にアクセスするのは twin_advance_clock と twin_run_action のみで、登録済みの URL に Webhook を配信します。
Claude Code:
claude mcp add fieldtwin -- npx -y fieldtwin mcp
その他の MCP クライアント(Cursor、Claude Desktop、VS Code)では、同じコマンドを JSON 設定に記述します。
{ "mcpServers": { "fieldtwin": { "command": "npx", "args": ["-y", "fieldtwin", "mcp"] } } }
これは 公式 MCP Registry に io.github.nickgeorgeseo/fieldtwin として登録されているため、レジストリを参照するクライアントやディレクトリでは、同じコマンドと設定を使ってインストールできます。
mcp の後にシナリオ名を追加すると、そのシナリオで起動します。MCP サーバーは独自の twin を起動し、FIELDTWIN_PORT で指定されていない場合はポート 4010 を使用します。そのため、先に同じポートで動作している別の twin を停止してください(claude mcp add -e FIELDTWIN_PORT=4011 ... で別のポートを指定できます)。エージェントがセッションを閉じると終了します。
MCP ツールが操作するのはテスト用のワールドだけです。エージェントは引き続き、本番コードと同じ方法で HTTP 経由でベンダー API を呼び出します。
問題が発生した場合
| 表示される内容 | 意味 |
|---|---|
fieldtwin: port 4010 is already in use | 別の twin(または別のプロセス)がそのポートを使用しています。FIELDTWIN_PORT=4011 npx fieldtwin |
| いずれかのベンダーから HTTP 401 が返る | twin がトークンを検証しています(FIELDTWIN_AUTH、シナリオ、または PUT /_twin/auth により auth が oauth になっています)。twin の OAuth エンドポイントからトークンを取得するか、any-token に戻してください。Housecall Pro も、Authorization ヘッダーが正確に Token <key> または Bearer <token> の形式でない場合、401 を返します |
Jobber で毎回 THROTTLED が返る | クエリが10,000ポイントの総予算を超えているため、実行できません。first または last を使ってノード数を減らしてください |
QuickBooks 5010 Stale Object | 更新時に古い SyncToken が送信されました。実際の API でも同様に拒否されます。レコードを再取得してから、もう一度試してください |
X-Twin-Fault ヘッダー | 意図的に障害ルールが発動しました。GET /_twin でアクティブなルールを確認できます |
| fieldtwin のバグに言及する 500 エラー | twin のログと GET /_twin/log.jsonl を添えて、issue を作成してください |
プロジェクト構成
src/
core/ the twin runtime: event log, clock, seeded randomness, OAuth server, vendor contract
world/ the vendor-neutral fake business: customers, price list, months of work history, drift
vendors/
quickbooks/ one folder per vendor, all with the same shape
jobber/
housecallpro/
faults/ makes vendor APIs misbehave on purpose
control/ /_twin HTTP routes, MCP tools, scenarios (one shared operations.ts)
app.ts wires it together
start.ts startTwin(): a twin on its own port, from code
index.ts what `import { startTwin } from "fieldtwin"` gets
cli.ts the fieldtwin command
server.ts entry point for `npm start`
mcp.ts entry point for `npm run mcp` and `fieldtwin mcp`
scenarios/ ready-made situations (heat-wave-monday, mismatched-books, flaky-apis)
examples/ code that uses the twins the way yours will: over HTTP, never importing src/
sync-jobber-invoice-to-quickbooks/ the reference integration, its demo, and its tests
conformance/ the same scripted sessions against a real sandbox and the twin, compared and scored
dashboard/ a web dashboard for a running twin (Next.js, a package of its own), over HTTP like examples/
test/ tests by vendor and area, plus core and the architecture guards
ARCHITECTURE.md では、全体の仕組みと、変更しやすさを保つための3つのルールを説明しています。core/ 内を変更する前に読んでください。
貢献
貢献を歓迎します。特に、ツインの忠実度を高めるものを歓迎します。CONTRIBUTING.mdでは、ベンダーのドキュメントページからテスト、FIDELITY.mdの項目まで、エンドポイントの追加手順を説明しています。ベンダーフォルダーはすべて同じ構成なので、1つ読めばほかも読めるようになります。QuickBooksのサンドボックス企業、Jobberのテストアカウント、またはテスト用に保持しているMAXプランかXLプランのHousecall Proアカウントがある場合、最も価値のある貢献は、そのアカウントを使って適合性テストスイートを記録することです。npm run conformance:recordを実行すると、ツインの推測が証拠に変わります。まず、ベンダーのAPI利用規約で許可されていることを確認してください(各規約へのリンク)。公開ドキュメントのみを使って構築してください。ベンダーのコード、非公開ドキュメント、実際の顧客データを貼り付けないでください。
今後の予定:入力に関する情報が公開ドキュメントに掲載され次第Jobberの書き込み機能を増やすこと、実際のサンドボックスでの記録を増やすこと、Housecall Proへの対応を拡充すること、その後、SamsaraやMotive、EDI取引パートナーなど、対応ベンダーを増やすこと、そして配車・運用エージェント向けのベンチマークを作成することです。ARCHITECTURE.mdにロードマップを記載しています。
免責事項とライセンス
fieldtwinは独立したプロジェクトです。Jobber、Intuit、Housecall Proとの提携関係はなく、各社から承認や支援を受けていません。Jobber、QuickBooks、Housecall Proは、それぞれの所有者の商標です。
著作権 2026 Nick George。Apache License 2.0に基づいてライセンスされています。
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。