Less Annoying CRM (remote, OAuth) について
Cloudflare Workers 上で動作する、Less Annoying CRM 向けの非公式リモート MCP サーバーです。Streamable HTTP と OAuth 2.1 に対応し、10 個のツールに読み取り専用または読み書き権限でアクセスできます。
主な機能
- 01OAuth 2.1 のブラウザー認可
- 02Streamable HTTP によるリモート接続
- 03連絡先・メモ・タスクの取得
- 04パイプラインと項目の確認
- 05レコード作成と項目ステータスの更新
- 06認可スコープによるアクセス制限
活用シーン
- 01claude.ai から CRM に接続
- 02ブラウザーでリモート MCP クライアントを認可
- 03CRM の照会に読み取り専用権限を付与
- 04自身の Cloudflare アカウントにデプロイ
MCP 対応
Streamable HTTP 経由で Less Annoying CRM の 10 個のツールを公開し、OAuth 2.1 のスコープで MCP クライアントのアクセスを制御します。
設定
リモートサーバーはインストール不要です。クライアントを選んで設定をコピーするか、ワンクリックで導入できます。
https://lacrm-mcp.northkeellabs.com/mcpclaude mcp add --transport http com-northkeellabs-lacrm-mcp-remote https://lacrm-mcp.northkeellabs.com/mcp{"mcpServers":{"com-northkeellabs-lacrm-mcp-remote":{"type":"http","url":"https://lacrm-mcp.northkeellabs.com/mcp"}}}Cursor で開く{"servers":{"com-northkeellabs-lacrm-mcp-remote":{"type":"http","url":"https://lacrm-mcp.northkeellabs.com/mcp"}}}VS Code で開く{"mcpServers":{"com-northkeellabs-lacrm-mcp-remote":{"type":"http","url":"https://lacrm-mcp.northkeellabs.com/mcp"}}}{"mcpServers":{"com-northkeellabs-lacrm-mcp-remote":{"serverUrl":"https://lacrm-mcp.northkeellabs.com/mcp"}}}コマンドはターミナルで実行し、JSON は各クライアントの MCP 設定に貼り付けて再起動します。認可が必要なサーバーは初回接続時にログインを求めます。
ツール
詳細
lacrm-mcp-remote
Less Annoying CRM(LACRM)用のリモート MCP サーバーです。Cloudflare Workers 上でホストされ、OAuth 2.1 サインインに対応しています。ローカルの lacrm-mcp サーバー(stdio、Python、こちらも Northkeel Labs によるもの)と同じ 10 個のツールを、Streamable HTTP 経由で提供するため、claude.ai のカスタムコネクターや、OAuth 対応のリモートサーバーをサポートするあらゆる MCP クライアントで利用できます。
独立した非公式プロジェクトです。Less Annoying CRM との提携、同社からの推奨やサポートは受けていません。
ホスト済みエンドポイント
https://lacrm-mcp.northkeellabs.com/mcp
Northkeel Labs は、このコードをその URL で実行しています。OAuth 対応のリモートサーバーをサポートする MCP クライアント(クライアントの接続を参照)に貼り付けてサインインし、同意画面で LACRM API キーを貼り付けてください。Worker が保存するのは暗号化された OAuth グラントのみです。キーや CRM データのコピーはほかに保持しません。無料、ベストエフォートで提供し、SLA はありません。1 接続あたり毎分 60 回のツール呼び出しに対応しています。独自の Cloudflare アカウントで実行する場合は、独自環境へのデプロイを参照してください。
リモート + OAuth を採用する理由
ローカルの stdio サーバーでは、ユーザーごとに Python をインストールし、設定ファイルに API キーを保存して、自分のマシンでプロセスを実行し続ける必要があります。リモートサーバーなら URL は 1 つだけです。ユーザーはその URL を claude.ai、Claude Desktop、または Cursor に貼り付け、ブラウザーで一度サインインするだけで、クライアントは有効期間が短く、取り消し可能なトークンを取得します。API キーはクライアントや設定ファイルに保存されず、モデルのコンテキストにも入りません。
LACRM には OAuth がなく、API キーのみがあります。このサーバーは「自分のキーを持ち込めば OAuth を利用できる」というパターンで両者を橋渡しします。Worker は標準準拠の OAuth 2.1 認可サーバー(ディスカバリーメタデータ、動的クライアント登録、Client ID Metadata Documents、PKCE S256、リフレッシュトークンのローテーション)であり、同意画面でユーザーが LACRM キーを一度貼り付けます。キーは読み取り専用の LACRM 呼び出し 1 回で検証され、その後は暗号化された OAuth グラント内にのみ保存されます。
アーキテクチャ
flowchart LR
subgraph Client["MCP client (claude.ai, Claude Desktop, Cursor, Inspector)"]
C[OAuth + Streamable HTTP]
end
subgraph Worker["Cloudflare Worker: lacrm-mcp-remote"]
OP["OAuthProvider<br/>discovery, register, token"]
AU["Consent page at authorize<br/>key + access level"]
MCP["MCP endpoint<br/>rate limit, scope gate, 10 tools"]
RL[("RateLimiter<br/>SQLite Durable Object<br/>one per grant")]
end
KV[("KV: OAUTH_KV<br/>clients, grants, token hashes<br/>props encrypted")]
LACRM["LACRM API v2<br/>api.lessannoyingcrm.com"]
C -- discovery, DCR/CIMD, token --> OP
C -- browser sign-in --> AU
C -- Bearer token --> MCP
OP <--> KV
AU -- GetUser (validate key) --> LACRM
AU -- completeAuthorization --> OP
MCP --> RL
MCP -- key from decrypted grant --> LACRM
コード構成:src/index.ts(OAuthProvider の配線)、src/authorize.ts(同意画面)、src/mcp.ts(保護されたエンドポイント)、src/tools.ts(10 個のツール)、src/lacrm.ts(API クライアント、リトライ、エラーマッピング)、src/security.ts(切り詰め、信頼できないデータのラッパー、秘匿化)、src/ratelimit.ts(Durable Object)。
@cloudflare/workers-oauth-provider 1.2.4、agents 0.33.0(createMcpHandler)、MCP TypeScript SDK v2(@modelcontextprotocol/server 2.4.0)を基盤としています。ハンドラーはステートレスです。リクエストごとに、そのグラント用に構築された新しい MCP サーバーが使われます。同じ /mcp エンドポイントで、2025 年の Streamable HTTP クライアントとステートレスな 2026 年プロトコルの両方に対応します。/sse エンドポイントはありません。McpAgent は agents 0.33.0 で非推奨となったため、代わりに createMcpHandler を使用しています。Durable Object は、グラントごとのレート制限用のものだけです。
認証フローの手順
- クライアントはトークンなしで
POST /mcpを呼び出し、WWW-Authenticate: Bearer resource_metadata=".../.well-known/oauth-protected-resource/mcp", scope="lacrm.read"を含む401を受け取ります。 - 保護リソースのメタデータ(RFC 9728)を読み取り、続いて
/.well-known/oauth-authorization-serverにある認可サーバーのメタデータ(RFC 8414)を読み取ります。 - Client ID Metadata Document URL を使うか、
/oauth/registerで登録(RFC 7591 の動的クライアント登録)して、自身を識別します。 - PKCE(S256 のみ)、
state、resourceを指定して、ブラウザーで/authorizeを開きます。サーバーは未知のクライアントや、登録されていないリダイレクト URI を拒否し、リダイレクトせずにエラーをその場で表示します。 - 同意ページには、要求元のクライアント(名前、CIMD クライアントの検証済みドメイン、リダイレクト先のホスト、ローカルアプリへの警告)が表示され、LACRM API キーとアクセスレベル(読み取り専用(
lacrm.read)、または読み取りと書き込み(lacrm.read lacrm.write))の入力を求めます。 - 送信時(POST のみ、CSRF ハンドルは
__Host-Cookie に紐付けられ、1 回のみ使用可能で、有効期間は 10 分)、サーバーはキーを使って LACRM のGetUserを呼び出します。キーが拒否された場合はエラーが表示され、何も保存されません。 - サーバーは認可を完了します。キー、LACRM ユーザー ID、付与されたスコープ、グラント ID はグラントプロパティとして保存され、トークンでラップされたキー素材を使って暗号化されます。ブラウザーはコードとともにクライアントへリダイレクトされます。
- クライアントは
/oauth/tokenでコードと PKCE 検証子を交換し、有効期間 1 時間のアクセストークンとローテーションするリフレッシュトークンを取得します。 /mcpのすべての呼び出しにはグラントごとにレート制限(デフォルトでは 1 分あたり 60 回)が適用され、スコープが確認されます。読み取り専用グラントではtools/listに表示されるツールは 6 個で、書き込みツールは一覧に表示されたかどうかにかかわらず、呼び出しが拒否されます。
ツール
| ツール | アクセス | LACRM 関数 | 機能 |
|---|---|---|---|
search_contacts | 読み取り | GetContacts | テキストで連絡先と企業を検索し、レコードごとの概要をページ分けして返します |
get_contact | 読み取り | GetContact | 1 件の連絡先または企業の完全なレコードを取得します |
list_contact_notes | 読み取り | GetNotesAttachedToContact | 連絡先に付属するメモをページ分けして取得します |
list_tasks | 読み取り | GetTasks | 日付範囲内のタスクを取得します(最大 366 日)。連絡先での絞り込みは任意です |
list_pipelines | 読み取り | GetPipelines | パイプラインとそのステータス ID を取得します |
list_pipeline_items | 読み取り | GetPipelineItems | 1 つのパイプライン内のアイテムを取得します。ステータスでの絞り込みは任意です |
create_contact | 書き込み | CreateContact | 連絡先または企業を新規作成します。所有者のデフォルトはキーのユーザーです |
add_note | 書き込み | CreateNote | 連絡先にプレーンテキストのメモを追加します |
create_task | 書き込み | CreateTask | タスクを新規作成します。連絡先への紐付けは任意です |
update_pipeline_item_status | 書き込み | EditPipelineItem | アイテムを別のステータスに移動します。自動化処理は実行されません |
削除ツールはありません。制限は Python 版と同じです。1 ページあたりの結果は最大 50 件で、has_more_results が付与されます。テキストフィールドは 2,000 文字で切り捨てられ、リストは 50 項目までに制限されます。また、CRM レコードを含むすべてのレスポンスには、その内容が指示ではなく信頼できないデータであることを示す data_notice が含まれます。LACRM にリクエストが送信される前に、入力は zod で検証されます(ID は数字の文字列、列挙値、ISO 日付、メール形式、長さ制限)。
独自にデプロイする
Cloudflare アカウント(無料プランで十分です。Worker 1 つ、KV 名前空間 1 つ、SQLite によってバックアップされた Durable Objects を利用できます)と Node 22 以降が必要です。
git clone <this repo> lacrm-mcp-remote && cd lacrm-mcp-remote
npm install
npx wrangler login
npx wrangler kv namespace create OAUTH_KV
表示された名前空間 ID を wrangler.jsonc の kv_namespaces[0].id に貼り付けます(リポジトリ内の ID はホスト済みインスタンスのものであり、ご自身のアカウントでは機能しません)。リポジトリの設定では、Worker は当社のカスタムドメインで配信されるため、アドレスも変更してください。
- 独自ドメインを使う場合(ゾーンは同じ Cloudflare アカウント上にある必要があります):
routes[0].patternをホスト名(例:mcp.example.com)に設定し、vars.MCP_RESOURCEをhttps://mcp.example.com/mcpに設定します。 - workers.dev を使う場合:
routesエントリを削除し、"workers_dev": trueに設定して、vars.MCP_RESOURCEをhttps://lacrm-mcp-remote.<your-workers-subdomain>.workers.dev/mcpに設定します(サブドメインは Cloudflare ダッシュボードの Workers and Pages に表示されています)。
トークンはこの正確な URL に紐付けられます。次に、以下を実行します。
npm test # unit and integration tests in the Workers runtime
npx wrangler deploy
サーバー側のシークレットは不要です。各ユーザーの LACRM キーは同意ページ経由で渡され、暗号化されたグラント内にのみ保存されます。オプション設定は wrangler.jsonc 内の通常の変数です(RATE_LIMIT_PER_MINUTE、デフォルトは 60)。後から独自のシークレットを追加する場合は、本番環境では npx wrangler secret put NAME を、ローカルでは .dev.vars を使用してください(.dev.vars.example を参照。.dev.vars は gitignore 対象です)。
ローカル開発:.dev.vars.example を .dev.vars にコピーします(本番環境の値を上書きし、MCP_RESOURCE を http://localhost:8787/mcp に設定します)。その後、npx wrangler dev を実行すると、すべてが http://localhost:8787 で配信されます。
クライアントを接続する
ホスト済みエンドポイント https://lacrm-mcp.northkeellabs.com/mcp またはご自身のデプロイ先 URL を使用します。
-
claude.ai:Settings > Connectors > Add custom connector を選択します。URL を貼り付けてから Connect を選択します。ブラウザーのタブが開いて同意ページが表示されるので、LACRM キー(LACRM: Settings > Programmer API)を貼り付けて、アクセスレベルを選択します。
-
Claude Desktop:Settings > Connectors > Add custom connector を選択します。URL と手順は claude.ai と同じです。
-
Cursor:
~/.cursor/mcp.jsonに追加してサーバーを有効にします。Cursor がサインイン用のブラウザーを開きます。{ "mcpServers": { "lacrm": { "url": "https://lacrm-mcp.northkeellabs.com/mcp" } } } -
MCP Inspector:
npx @modelcontextprotocol/inspectorを実行し、トランスポートとして「Streamable HTTP」を選択して URL を入力し、OAuth サインインの手順を完了します。ローカル開発環境に接続する場合はhttp://localhost:8787/mcpを使用します。
アクセスを取り消すには、LACRM で API キーを削除するか、ローテーションします。そのキーを使って作成されたすべてのグラントは、トークンを保持しているクライアントにかかわらず機能しなくなります。
セキュリティ
概要(詳細と脅威モデルについては SECURITY.md を参照してください):
- LACRM キーがツール引数として受け入れられることはなく、クライアントに返されることも、ログに記録されることもありません。LACRM からのエラーテキストは、ツールの結果に渡される前に秘匿化されます。
- キーを保持するグラントのプロパティは OAuth プロバイダーによって暗号化されます。アクセストークン、リフレッシュトークン、コードはハッシュとしてのみ保存されます。
- 同意ページ:POST のみを受け付け、ブラウザーに紐付けられた単回使用の CSRF ハンドルを使用します。クライアントから渡されたすべてのテキストは HTML エスケープされます。
Content-Security-Policy: default-src 'none'; script-src 'none'; frame-ancestors 'none'を設定し、nonce 付きのインラインスタイルを 1 つだけ許可します。JavaScript とサードパーティーリソースは使用せず、X-Frame-Options: DENYとno-storeを設定します。 - リダイレクト URI は完全一致で照合されます。不明なクライアントや未登録のリダイレクト URI はローカルエラーとして表示され、リダイレクトされることはありません。
- すべての呼び出しでスコープを適用し、グラントごとにレート制限を設けます。書き込みは 5xx エラーやタイムアウト時に自動再試行されません。
- プロンプトインジェクションのリスクを低減するため、CRM のコンテンツはサイズを制限し、信頼できないデータとしてラベル付けされます。無害化することはできないため、書き込みには必ず人間が関与し、要約には読み取り専用グラントを優先してください。
テスト
テスト結果(2026-11-10):npm test は50件のテストに合格しました(@cloudflare/vitest-plugin 経由のWorkersランタイム、fetch でLACRMをモック)。tsc --noEmit はエラーなし、wrangler deploy --dry-run は成功しました。実環境でのエンドツーエンドテスト(wrangler dev とデモデータを含む実際のLACRMアカウントに対して scripts/e2e-live.mjs を実行)は20/20ステップに合格し、本番環境へのデプロイに対しても同じく20/20ステップに合格しました(https://lacrm-mcp.northkeellabs.com)。機密情報を伏せた出力:ローカル、本番環境。また、2026-11-10にclaude.aiのカスタムコネクターとしてもテストしました(読み取り専用の権限)。
テストでは、各ツールのリクエストマッピングと出力形式、切り詰め、信頼できないデータのラッパー、エラーのマッピングと機密情報のマスキング、再試行ポリシー、スコープ制御(読み取り権限では書き込みツールを参照も実行もできず、バッチ内でも同様です)、同意ページ(不正なキー、CSRFトークンの欠落や偽造、不明なリダイレクトURI、不明なクライアント、HTMLエスケープ、拒否、使い捨てハンドル)、レート制限、キーがレスポンスやログ行に一切表示されないことを検証します。
ライブテストを自分で実行するには、npx wrangler dev を起動してから、LACRM_API_KEY=... node scripts/e2e-live.mjs を実行してください。書き込み操作を1回行います。連絡先「Maya Demo」にメモを追加します。
免責事項
これは独立した非公式プロジェクトです。Less Annoying CRMとは提携しておらず、同社による承認やサポートも受けていません。「Less Annoying CRM」はその所有者の商標です。自己責任でご利用ください。本番環境のCRMアカウントに接続する前に、コードを確認してください。MITライセンスです。LICENSEを参照してください。
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。