sob-connect-mcp
v0.4.0
Published
MCP server exposing SOB's Connect API (end-user CRUD, access levels, user types, passwords, login) as tools for AI agents.
Readme
sob-connect-mcp
An MCP (Model Context Protocol) server that gives an AI agent — Claude Desktop, Claude Code, Cursor, or any other MCP client — direct, schema-validated tools for SOB's user-management surface: end-user CRUD, access levels, passwords, email templates, ad-hoc email, and Sync Target (partner webhook) management.
Built on top of the sob-connect-sdk package for
the connect_* tier — this project only adds the MCP tool-schema layer, retry/idempotency/
error-mapping is inherited from the SDK.
Two separate credential tiers, matching two separate backend security boundaries:
connect_*(13 tools) — the Connect API + public embed-auth endpoints, authenticated with a Connect API token (connect:{id}ability). This is the partner-facing surface.admin_*(67 tools) — workspace/access-level/user-type/tag/end-user/staff CRUD, Sync Targets, email templates, passwords, permissions, the audit log, platform settings, Connect token management, authenticated with a separate, higher-privilege admin Personal Access Token. Only registered if that credential is configured — omit it and the admin tier simply doesn't exist for that server instance.
Design philosophy: primitives, not workflows
This server does not (and deliberately will not) expose bespoke tools like clone_workspace or
diff_workspaces. "Copy X from workspace A to B", "find what's different between these two
workspaces", "clean up duplicate endpoints" — these are handled by the calling agent composing the
list/get/create primitives below at conversation time, not by special-cased server logic. A
rigid clone_workspace tool can't anticipate "clone everything except the auth secret" vs "clone
only the access levels" vs "clone but rename it" — an agent reasoning over complete primitives can.
What actually makes that composition reliable:
- Every resource has full list/get/create/update/delete coverage — nothing requires falling back to the admin UI mid-workflow.
- List tools accept name/status/etc. filters, not just raw pagination — an agent can find "the Project Manager workspace" without you supplying its numeric id.
- Tool descriptions state relationships and prerequisites (e.g. sync tiers need an access level to exist first) so an agent sequences multi-step requests correctly on the first try.
- Destructive/high-risk actions require
confirm: true— the one guardrail that is special-cased, since no amount of clever composition should make a delete accidental.
What this can and can't do
One Connect token maps to one or more specific workspaces (connect:{company_id} abilities) with
full CRUD on each — there is no finer-grained read-only vs read-write split at the API level.
If you don't want an agent able to delete users, don't grant it a token, full stop; this server's
own guardrail is that connect_delete_user requires an explicit confirm: true, not a permission
tier.
Several tools have real side effects on external systems: connect_upsert_user,
connect_change_user_role, and connect_change_user_status can fire the workspace's welcome/access
emails and any configured outbound Sync Target webhooks to partner platforms. This is not a
sandbox — every call is a real action against the live workspace your token is scoped to.
admin_test_sync_endpoint fires a real, live HTTP request to a partner's system using an actual
end-user's data — also not a dry run. admin_reveal_user_password returns a real customer's
plaintext password (audit-logged server-side, ADR-0003). Both, plus every delete/revoke tool,
require an explicit confirm: true argument — the agent cannot trigger them accidentally as a side
effect of a loosely-worded request.
sync_targets.auth_config (the credential SOB uses to call a partner's webhook) is redacted on
every read — admin_list_sync_targets/admin_get_sync_target never return the real secret value,
only whether one is configured. It's still accepted as write-only input to
admin_create_sync_target/admin_update_sync_target, since setting it requires the value to pass
through at least once — but it never comes back out.
Setup
There are two setups depending on who's using this instance. Pick one.
Option A — personal/internal use (simplest, one token for everything)
For SOB staff using this themselves across any/all workspaces, including ones created later. This
works because ConnectApiAuth accepts a wildcard-ability token for any workspace, not just
admin routes — one token, both tiers, every workspace, no re-issuing when a new workspace is added.
- Mint one general-purpose admin token:
POST /api/v1/auth/api-tokenswhile logged into the admin panel, body{"name": "sob-connect-mcp"}— returns a'*'-ability token once. This carries no more access than your existing session; every route still checks your real Spatie permissions independent of the token. - Set it as
SOB_TOKEN— one env var backs bothconnect_*andadmin_*tools, so you never pass the same token twice.
Option B — handing this to a partner/external developer (scoped)
For anyone who should only ever touch their own workspace(s) — no admin tier, no wildcard.
- An admin creates a Connect token at
POST /api/v1/workspaces/{id}/connect-tokensin the admin panel — the response includes a ready-to-usesdk_configblock withslug,base_url, andtoken. Passadditional_company_ids: [id, id, ...]in that request to cover more than one workspace with one token. - Give them only
SOB_CONNECT_TOKEN/SOB_BASE_URL/SOB_CONNECT_SLUG— leaveSOB_ADMIN_TOKENunset. Theadmin_*tools simply won't exist for their server instance.
Connecting to your MCP client
Claude Code — one command, no config file editing:
claude mcp add sob-connect \
-e SOB_TOKEN="<your wildcard token>" \
-e SOB_BASE_URL="https://devapi.saasonboard.com" \
-e SOB_CONNECT_SLUG="project-manager" \
-- npx -y sob-connect-mcp(Swap the last line for -- node /absolute/path/to/sob-connect-mcp/dist/server.js to run from a
local clone instead of the published package.) Add --scope user to make it available in every
project instead of just the current one. Start a new Claude Code session afterward — MCP
servers only load at session startup.
Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json on macOS) /
Cursor (.cursor/mcp.json) — same shape as a JSON config entry:
{
"mcpServers": {
"sob-connect": {
"command": "npx",
"args": ["-y", "sob-connect-mcp"],
"env": {
"SOB_TOKEN": "10|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"SOB_BASE_URL": "https://devapi.saasonboard.com",
"SOB_CONNECT_SLUG": "project-manager"
}
}
}
}(Use "command": "node", "args": ["/absolute/path/to/dist/server.js"] instead to run from a local
clone.)
Option B (scoped, no admin tier) skips SOB_TOKEN entirely — set SOB_CONNECT_TOKEN instead,
and leave SOB_ADMIN_TOKEN unset.
Restart your client and start a new session — ask it to list users in your workspace to confirm the connection works end to end.
Environment variables
| Variable | Required | Description |
| ------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| SOB_TOKEN | One of this or SOB_CONNECT_TOKEN | Single wildcard ('*'-ability) token backing both tool tiers on every workspace (Option A). Simplest setup. |
| SOB_CONNECT_TOKEN | One of this or SOB_TOKEN | Scoped connect:{id} token for one or more specific workspaces (Option B). Overrides SOB_TOKEN if both are set. |
| SOB_BASE_URL | Yes | The SOB backend's base URL, e.g. https://devapi.saasonboard.com. |
| SOB_CONNECT_SLUG | No | Default workspace slug used when a tool call omits workspace_slug. |
| SOB_ADMIN_TOKEN | No | Explicit admin PAT, only if you want a different token than SOB_TOKEN for the admin tier. Overrides SOB_TOKEN if both are set. |
If a required variable is missing, the server prints a clear error to stderr and exits immediately rather than hanging silently.
Tools
connect_* (13 — partner-facing, Connect token)
| Tool | Description |
| ----------------------------------- | ------------------------------------------------------------------- |
| connect_get_workspace_config | Workspace branding + full access-level/user-type lists in one call. |
| connect_list_access_levels | List active access levels (plan tiers). |
| connect_list_user_types | List active user types. |
| connect_list_users | List/filter/paginate end-users. |
| connect_get_user | Fetch a single user by email. |
| connect_upsert_user | Create or update a user (matched by email). |
| connect_update_user | Update name/first/last/password (not role/status). |
| connect_change_user_role | Change a user's access level. |
| connect_change_user_status | Set or toggle active/inactive. |
| connect_record_user_login | Record a login event (does not fire outbound sync). |
| connect_delete_user | Soft-delete a user. Requires confirm: true. |
| connect_trigger_password_reminder | Email the user their current password (no reset-token flow). |
| connect_verify_login | Check whether an email/password pair authenticates. |
Every connect_* tool accepts an optional workspace_slug (falls back to SOB_CONNECT_SLUG if set).
admin_* (67 — staff-facing, admin PAT, only registered if SOB_ADMIN_TOKEN is set)
Every admin_* tool takes a numeric workspace_id (not slug — admin routes are id-keyed). Use
admin_list_workspaces to look one up — pass name to search instead of scanning the full list.
This tier covers the full path from an empty platform to a live partner integration: create the workspace, its access levels/user types/tags, its staff and end-users, then the Sync Target/endpoints/tiers that wire it to a partner — no admin UI required for any step.
| Group | Tools |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Workspaces | admin_list_workspaces (filters: name/status/owner_email/plan_id), admin_get_workspace, admin_create_workspace, admin_update_workspace, admin_delete_workspace (⚠️ confirm) |
| Access levels | admin_list_access_levels (filters: name/status), admin_get_access_level, admin_create_access_level, admin_update_access_level, admin_delete_access_level (⚠️ confirm) |
| User types | admin_list_user_types (filters: name/status), admin_get_user_type, admin_create_user_type, admin_update_user_type, admin_delete_user_type (⚠️ confirm) |
| Tags & tag groups | admin_list_tag_groups, admin_create_tag_group, admin_update_tag_group, admin_delete_tag_group (⚠️ confirm), admin_list_tags, admin_create_tag, admin_update_tag, admin_delete_tag (⚠️ confirm) |
| End-users | admin_list_users (filters: search/access_level_id/user_type_id/status/is_trial/is_expired/login_type), admin_get_user, admin_create_user, admin_update_user, admin_delete_user (⚠️ confirm), admin_toggle_user_status, admin_bulk_import_users (up to 500 at once, JSON — no file upload) |
| Staff & permissions | admin_list_permissions (read-only catalog), admin_list_staff, admin_get_staff, admin_create_staff, admin_update_staff, admin_delete_staff (⚠️ confirm) |
| Audit log | admin_list_audit_log (read-only — who did what, when, filterable by workspace/staff member/event) |
| Platform settings | admin_get_platform_settings, admin_update_platform_settings (⚠️ confirm — global, affects every workspace) |
| Sync targets | admin_list_sync_targets, admin_get_sync_target, admin_create_sync_target, admin_update_sync_target, admin_delete_sync_target (⚠️ confirm) |
| Sync endpoints | admin_list_sync_endpoints, admin_create_sync_endpoint, admin_update_sync_endpoint, admin_delete_sync_endpoint (⚠️ confirm), admin_preview_sync_endpoint (dry run, no confirm needed), admin_test_sync_endpoint (⚠️ confirm, live request) |
| Sync tiers | admin_create_sync_tier, admin_update_sync_tier, admin_delete_sync_tier (⚠️ confirm) |
| Sync logs | admin_list_sync_logs (read-only) |
| Connect tokens | admin_list_connect_tokens, admin_create_connect_token, admin_revoke_connect_token (⚠️ confirm) |
| Passwords | admin_reveal_user_password (⚠️ confirm, audit-logged), admin_set_user_password |
| Email templates | admin_list_email_templates, admin_create_email_template, admin_update_email_template, admin_delete_email_template (⚠️ confirm) |
| Ad-hoc / resend email | admin_send_user_email, admin_bulk_send_email, admin_resend_welcome_email, admin_resend_access_email |
admin_create_sync_endpoint/admin_update_sync_endpoint also accept response_extraction_path —
a dot-path into the partner's JSON response body to capture as the user's external id, available
in later templates as {{external_id}}.
Field mapping: url_path/headers/body_template/query_params all support {{variable}}
tokens, resolved per-user at fire time — the full catalog ({{user.email}}, {{user.first_name}},
{{access_level.external_value}}, {{external_id}}, {{auth.token}}, and more) is embedded
directly in admin_create_sync_endpoint's tool description, not something you have to guess or dig
up separately. This is a completely different token syntax from email templates, which use
[bracket] tokens instead — never mix the two up. Use admin_preview_sync_endpoint to see the
fully resolved request (with secrets redacted) before admin_test_sync_endpoint fires it for real.
Two independent directions: a Connect token (admin_create_connect_token) is the partner → SOB
direction; a Sync Target is the SOB → partner direction. Most real integrations want both, but
they're separate steps and neither implies the other. If the same partner is on both ends, a
partner-initiated create will immediately echo back out to them via any matching Sync Target
endpoint — their system needs to handle that idempotently, or you only wire up one direction.
Every new workspace already ships with a working example: admin_create_workspace auto-seeds
an active "httpbin Test (Live)" Sync Target with all 5 real trigger endpoints wired to
httpbin.org (a public request-echoing service) — it fires for real on every actual event from day
one. Use admin_list_sync_logs against it to confirm the whole pipeline works before configuring a
real partner, or just read its endpoints as a working template reference.
Staff vs. end-users: "staff" are platform accounts with Spatie roles (people who log into the
admin panel); "end-users" (admin_*_user) are your customers' customers, scoped to one workspace,
and now also carry first_name/last_name (used by {{user.first_name}}/{{user.last_name}} in
sync templates) and external_refs (the id a partner assigned this user, once
response_extraction_path has captured one). "Give this person the same permissions as John" means
reading John's roles via admin_get_staff and passing that same array to
admin_create_staff/admin_update_staff — roles are the unit of permission-granting, and
admin_update_staff's roles field replaces the set, it doesn't add to it.
Accountability: admin_list_audit_log answers "who did X and when" across the whole platform
(filterable by workspace via company_id, by staff member via causer_id, or by event type) — use
it before assuming something is a bug; it might just be a staff member's deliberate change.
Platform settings are NOT workspace-scoped — admin_update_platform_settings affects every
workspace and customer simultaneously (in particular maintenance_mode: true takes the whole
platform offline). It requires confirm: true like any destructive tool, but be extra deliberate
here specifically because "just testing" language in a request could otherwise slip through to a
truly global toggle.
Bulk-importing users: admin_bulk_import_users takes a JSON array (up to 500 rows), matched
and created-or-updated by email — the same semantics as the admin UI's CSV importer, but built as a
new backend endpoint specifically so no file upload is needed. "Import these 50 people into Test V"
works directly; you don't need an actual CSV file.
Not covered on purpose: the original file-upload CSV import/export/sample-download endpoints
still exist in the backend but aren't wrapped here — multipart file transfer doesn't fit MCP's JSON
tool-call shape, and admin_bulk_import_users/admin_list_users cover the same ground without it.
Payment/IPN webhook config, pricing/plans/bundles, campaigns, and onboarding/tutorial content are
separate SaaS-monetization features outside this server's "user connections and admin functionality"
scope — ask if you want those covered too.
Complete integration walkthrough
End-to-end example: integrating a brand-new workspace ("Test V") with a partner system ("Acme CRM"), start to finish, using only conversational prompts.
1. Create the workspace.
"Create a workspace called Test V"
This alone seeds 5 access levels, 5 user types, 4 tag groups with tags, welcome/forgot-password/
access-level email templates, and a fully working, active Sync Target
("httpbin Test (Live)") with all 5 trigger endpoints already wired to httpbin.org. Nothing else
is required to have a working sync pipeline you can inspect.
2. Verify the baseline before touching anything real.
"Create a test user in Test V, then show me the sync logs for the httpbin target"
Confirms the whole dispatch chain (model event → queued job → HTTP fire → log row) works before you configure a real partner.
3. Decide which direction(s) you need, and set them up independently:
"Create a Connect API token for Test V named 'Acme CRM'" — inbound: lets Acme push users into SOB.
"Create a sync target called Acme CRM in Test V, base_url https://api.acme.com, bearer auth with this token: sk_live_..." — outbound: SOB pushes users to Acme.
4. Configure endpoints with real field mapping, then verify before going live:
"Add a user_created endpoint to the Acme CRM sync target, POST to /v1/contacts, body: email is {{user.email}}, name is {{user.name}}, plan is {{access_level.external_value}}"
"Preview that endpoint for a real user" — see the exact resolved request, no live call.
"Now test-fire it for real" (⚠️ confirm) — same request, actually sent.
5. Map access levels to the partner's tier codes:
"Map the Premium access level to external value 'premium_tier' on the Acme CRM sync target"
6. Load users:
"Import these 50 people into Test V as Premium users: [list]" — or let Acme push them via their Connect token instead.
7. Clean up:
"Deactivate the httpbin test sync target now that Acme CRM is live" — stops the demo noise without losing it as a reference; delete it instead if you're sure you'll never need it again.
Two behaviors worth knowing going in, both documented directly in the relevant tool descriptions so the agent applies them automatically rather than you needing to remember them:
- Every user update fires both its specific trigger (e.g.
access_level_changed) anduser_updated— don't configure the same partner action on both or it double-fires. - If Acme is on both ends (their own Connect token and a Sync Target), a partner-initiated create immediately echoes back out to them — fine if their system is idempotent, worth avoiding otherwise.
Example prompts
None of these need exact tool names, ids, correct spelling, or complete grammar — describe what you want and the agent resolves it to the right tool(s), asking a clarifying question only when the ambiguity is real (which of two same-named workspaces, or anything destructive). Organized simple → advanced; use this as a test script to exercise every capability.
Lookups & reporting
- "List all workspaces"
- "Find the workspace called Project Manager" / "which workspace is 'proj manager'" (typo-tolerant)
- "How many users does Test V have?"
- "Show me inactive users in Project Manager"
- "Who hasn't logged in yet in the Test V workspace?"
- "Show me trial users whose access has expired"
- "What access levels exist in Project Manager?"
- "List the tags in this workspace, grouped by tag group"
- "Show me John's roles and permissions"
- "What permissions does the 'support' role grant?"
- "List every staff member with the super-admin role"
- "List every sync target across all my workspaces" (agent loops
admin_list_workspaces→admin_list_sync_targetsper id) - "What are the current platform settings?"
- "Is maintenance mode on?"
Basic CRUD
- "Create a workspace called Test V"
- "Add a Premium access level to Test V, $49/month"
- "Update the Premium access level's price to $59"
- "Create a user type called Trial User"
- "Add a tag group called Region with tags US, EU, APAC"
- "Rename the 'US' tag to 'United States'"
- "Create a user [email protected] in Test V with the Premium access level"
- "Import these 50 people into Test V as Premium users: [paste a list of names/emails]"
- "Update [email protected]'s phone number"
- "Deactivate user [email protected]" / "toggle jane's status"
- "Change [email protected]'s access level to Premium"
- "Reveal the password for user 12345" (⚠️ confirm, audit-logged)
- "Reset the password for [email protected] to a new one"
- "Delete the 'old-trial' access level" (⚠️ confirm)
- "Delete the Test V workspace, I'm done with it" (⚠️ confirm — soft delete, recoverable)
Sync targets / partner integrations
- "Create a sync target called Acme CRM in Test V pointing to https://api.acme.com using bearer auth with this key: ..."
- "Add a user_created endpoint to that sync target, POST to /v1/contacts, with this body: ..."
- "Also add a user_deleted endpoint that fires the same way"
- "Capture the partner's returned id as the external id, it's at data.id in the response"
(
response_extraction_path) - "Map the Premium access level to external value 'premium_tier'"
- "Show me what that endpoint's request would actually look like for [email protected]" (dry run, no live call)
- "Test-fire that endpoint" (⚠️ confirm — real HTTP request)
- "Show me the last 10 sync logs for that target"
- "Why did that last sync attempt fail?"
- "What's this user's id over in Acme CRM?" (reads
external_refs) - "Turn off that sync target without deleting it" (
is_active: falsevia update) - "Rotate the auth token on the Acme CRM sync target to this new value: ..."
Staff & permissions
- "Give [email protected] the same roles as [email protected]"
- "Create a staff account for [email protected] with the support role"
- "What can a 'support' role actually do?" (reads
admin_list_permissions+ the role's grants) - "Add the billing role to sarah's account, on top of what she already has" (read current roles first, since updates replace the set)
- "Remove admin access from the old contractor's account"
- "Delete mike's staff account" (⚠️ confirm)
Accountability & platform-level
- "Who deleted the user with email [email protected]?"
- "Show me everything that happened in the Test V workspace today"
- "What has staff member 12 changed recently?"
- "Has anyone viewed end-user passwords this week?"
- "Change the platform's default from-email address" (⚠️ confirm — affects every workspace)
- "Turn on maintenance mode" (⚠️ confirm — will be flagged as platform-wide before executing)
Fuzzy / incomplete requests (fault tolerance)
These are intentionally underspecified, misspelled, or missing detail — the agent should still figure out the intent, or ask one targeted question rather than failing outright:
- "add a acess level calld Premum to test v" (typos, still resolves)
- "make a new workspace like the pm one" ("Project Manager" inferred from context)
- "that guy john needs the same access as sarah" (resolves to staff roles or end-user access level depending on whether John/Sarah are staff or end-users — agent should ask if genuinely ambiguous)
- "kill the sync target" (agent should ask which one, and require confirm before deleting)
- "fix the broken integration for test v" (agent should investigate sync logs first, then propose a fix rather than guessing blindly)
- "how many people signed up this month" (maps to end-user list filtered/sorted by created_at)
- "turn off that user" (ambiguous between delete and deactivate — agent should ask, since one is reversible and one isn't)
Multi-step / combining operations
All of these are the agent chaining the primitives above at conversation time — none of them are single dedicated tools (see "Design philosophy" above):
- "Create a new workspace called Test V and copy all sync targets and endpoints from Project Manager" (secrets excluded — see the auth_config note above; you'll be asked to supply those)
- "Find the Project Manager workspace and copy its complete configuration to a new workspace"
- "Show me what's different between Project Manager and Test V" (access levels, user types, sync targets, tags — compared side by side)
- "Update all endpoints related to user creation across every sync target in Test V"
- "Find users with no access level assigned in any workspace and tell me which ones"
- "Find duplicate tags in this workspace and clean them up" (⚠️ confirm on each delete)
- "Find sync endpoints with the same trigger and URL on the same target — looks like duplicates"
- "Fix any workspace missing a default access level"
- "Set up a complete new workspace like Project Manager, but call it Test V and point its sync target at a different URL"
- "Deactivate every end-user across all workspaces who hasn't logged in in the last few months and give me a summary of what you changed"
- "Audit every sync target's auth_type across all workspaces and flag any still set to 'none'"
- "Give the new hire the same staff role AND set them up with access to only the Test V workspace" (staff role copy + a scoped Connect token, if that's what "access to only X" means here — the agent should clarify which)
- "Something changed in Test V yesterday and I don't know what — investigate" (audit log, scoped by company_id and date, then summarized)
If a request is ambiguous (which workspace? which of two similarly-named access levels? delete vs. deactivate?), the agent asks rather than guessing on anything destructive or hard to reverse.
Development
npm install
npm run build # compiles to dist/
npm run typecheck
npm run lint
npm testManual, protocol-level testing without a full MCP client:
npx @modelcontextprotocol/inspector node dist/server.jsRoadmap
Phases 1 and 2 (80 tools total: 13 connect_* + 67 admin_*) are built and published to npm.
Verified locally (typecheck/lint/format/build/unit tests, 92 tests) and against the live dev
backend — including confirming auth_config redaction against a sync target with a real live
secret. Phase 2 was extended four times: workspace and access-level/user-type CRUD; end-user CRUD
(admin tier), tags/tag groups, staff accounts, and the permission catalog; the audit log and
platform settings; and most recently a deep pass on the sync-target/field-mapping flow itself —
first_name/last_name on admin-created users (closing a real inconsistency with the
default-seeded demo sync target's own templates), external_refs visibility, the full
{{variable}} catalog surfaced directly in tool descriptions, and admin_preview_sync_endpoint
for safe dry-run field-mapping. A brand-new workspace, its users, and its partner integration can
now be fully configured through MCP alone, without the admin UI, with a working example sync
target from the moment the workspace exists.
Phase 3 (optional, only if needed): a multi-tenant Streamable HTTP transport for one shared hosted
server serving many separate, mutually-untrusted partners — a different problem than the
multi-workspace support already built into Phase 1's workspace_slug design. Not started.
