feat: REST API, MCP server, and team-scoped API tokens#136
feat: REST API, MCP server, and team-scoped API tokens#136ManukMinasyan merged 160 commits intomainfrom
Conversation
Introduce a shared Actions layer for business logic (CRUD for companies, people, opportunities, tasks, notes) consumed by both a versioned REST API (Sanctum auth, rate limiting, team scoping) and a Laravel MCP server with 20 tools, a schema resource, and overview prompt. Includes SetApiTeamContext middleware that bridges Sanctum auth to the web guard so observers and TeamScope work unchanged, API Resources with custom field serialization, and 58 feature tests covering both surfaces. Closes #85, closes #89
- Add App\Mcp and App\Http\Resources to arch test ignore lists (MCP classes extend framework base classes, Resources extend JsonResource) - Apply Rector rules: abort_unless(), query builder methods, arrow functions, class constants, removed unnecessary parentheses
There was a problem hiding this comment.
Pull request overview
Adds a Sanctum-authenticated REST API (v1) and an MCP server to expose core CRM entities (companies, people, opportunities, tasks, notes) to external integrations and AI agents, while attempting to preserve existing tenant scoping/policy behavior via a dedicated API team-context middleware.
Changes:
- Introduces
/api/v1/*CRUD endpoints with controllers, form requests, and JSON resources. - Adds an MCP server (
/mcp/relaticle) with tools for CRUD/listing, plus a CRM schema resource and overview prompt. - Adds an Actions layer for shared business logic + rate limiting configuration + feature tests.
Reviewed changes
Copilot reviewed 85 out of 85 changed files in this pull request and generated 20 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Feature/Mcp/RelaticleServerTest.php | MCP server/tool smoke tests for listing and CRUD behavior. |
| tests/Feature/Api/V1/TasksApiTest.php | REST API tests for tasks CRUD, auth, and team scoping. |
| tests/Feature/Api/V1/PeopleApiTest.php | REST API tests for people CRUD, auth, and team scoping. |
| tests/Feature/Api/V1/OpportunitiesApiTest.php | REST API tests for opportunities CRUD, auth, and team scoping. |
| tests/Feature/Api/V1/NotesApiTest.php | REST API tests for notes CRUD, auth, and team scoping. |
| tests/Feature/Api/V1/CompaniesApiTest.php | REST API tests for companies CRUD, auth, and team scoping. |
| tests/Feature/Api/V1/ApiTeamScopingTest.php | Tests switching teams via X-Team-Id and rejection cases. |
| routes/api.php | Registers versioned API resources with auth/scoping/rate limit middleware. |
| routes/ai.php | Registers MCP web endpoint protected by Sanctum + team context middleware. |
| phpstan.neon | Suppresses PHPStan attribute warnings under app/Mcp. |
| bootstrap/app.php | Prepends API throttling middleware to the API middleware group. |
| app/Providers/AppServiceProvider.php | Defines RateLimiter::for('api') limiter configuration. |
| app/Observers/TaskObserver.php | Uses default auth context when setting creator/team on create. |
| app/Observers/PeopleObserver.php | Uses default auth context when setting creator/team on create. |
| app/Observers/OpportunityObserver.php | Uses default auth context when setting creator/team on create. |
| app/Models/Scopes/TeamScope.php | Uses default auth context for whereBelongsTo(currentTeam). |
| app/Mcp/Tools/Task/UpdateTaskTool.php | MCP tool to update tasks via Actions + resource output. |
| app/Mcp/Tools/Task/ListTasksTool.php | MCP tool to list tasks via Actions + resource output. |
| app/Mcp/Tools/Task/DeleteTaskTool.php | MCP tool to delete tasks via Actions. |
| app/Mcp/Tools/Task/CreateTaskTool.php | MCP tool to create tasks via Actions. |
| app/Mcp/Tools/People/UpdatePeopleTool.php | MCP tool to update people via Actions + resource output. |
| app/Mcp/Tools/People/ListPeopleTool.php | MCP tool to list people via Actions + resource output. |
| app/Mcp/Tools/People/DeletePeopleTool.php | MCP tool to delete people via Actions. |
| app/Mcp/Tools/People/CreatePeopleTool.php | MCP tool to create people via Actions. |
| app/Mcp/Tools/Opportunity/UpdateOpportunityTool.php | MCP tool to update opportunities via Actions + resource output. |
| app/Mcp/Tools/Opportunity/ListOpportunitiesTool.php | MCP tool to list opportunities via Actions + resource output. |
| app/Mcp/Tools/Opportunity/DeleteOpportunityTool.php | MCP tool to delete opportunities via Actions. |
| app/Mcp/Tools/Opportunity/CreateOpportunityTool.php | MCP tool to create opportunities via Actions. |
| app/Mcp/Tools/Note/UpdateNoteTool.php | MCP tool to update notes via Actions + resource output. |
| app/Mcp/Tools/Note/ListNotesTool.php | MCP tool to list notes via Actions + resource output. |
| app/Mcp/Tools/Note/DeleteNoteTool.php | MCP tool to delete notes via Actions. |
| app/Mcp/Tools/Note/CreateNoteTool.php | MCP tool to create notes via Actions. |
| app/Mcp/Tools/Company/UpdateCompanyTool.php | MCP tool to update companies via Actions + resource output. |
| app/Mcp/Tools/Company/ListCompaniesTool.php | MCP tool to list companies via Actions + resource output. |
| app/Mcp/Tools/Company/DeleteCompanyTool.php | MCP tool to delete companies via Actions. |
| app/Mcp/Tools/Company/CreateCompanyTool.php | MCP tool to create companies via Actions. |
| app/Mcp/Servers/RelaticleServer.php | MCP server wiring for tools/resources/prompts. |
| app/Mcp/Resources/CrmSchemaResource.php | Provides a JSON schema-like description of CRM entities/fields. |
| app/Mcp/Prompts/CrmOverviewPrompt.php | Provides a human-readable CRM overview prompt. |
| app/Http/Resources/V1/UserResource.php | Serializes user info for nested API resource responses. |
| app/Http/Resources/V1/TaskResource.php | Serializes tasks including custom fields and relationships. |
| app/Http/Resources/V1/PeopleResource.php | Serializes people including custom fields and relationships. |
| app/Http/Resources/V1/OpportunityResource.php | Serializes opportunities including custom fields and relationships. |
| app/Http/Resources/V1/NoteResource.php | Serializes notes including custom fields and relationships. |
| app/Http/Resources/V1/Concerns/FormatsCustomFields.php | Shared formatting of custom field values into key/value JSON. |
| app/Http/Resources/V1/CompanyResource.php | Serializes companies including custom fields and relationships. |
| app/Http/Requests/Api/V1/UpdateTaskRequest.php | Validation rules for updating tasks via API. |
| app/Http/Requests/Api/V1/UpdatePeopleRequest.php | Validation rules for updating people via API. |
| app/Http/Requests/Api/V1/UpdateOpportunityRequest.php | Validation rules for updating opportunities via API. |
| app/Http/Requests/Api/V1/UpdateNoteRequest.php | Validation rules for updating notes via API. |
| app/Http/Requests/Api/V1/UpdateCompanyRequest.php | Validation rules for updating companies via API. |
| app/Http/Requests/Api/V1/StoreTaskRequest.php | Validation rules for creating tasks via API. |
| app/Http/Requests/Api/V1/StorePeopleRequest.php | Validation rules for creating people via API. |
| app/Http/Requests/Api/V1/StoreOpportunityRequest.php | Validation rules for creating opportunities via API. |
| app/Http/Requests/Api/V1/StoreNoteRequest.php | Validation rules for creating notes via API. |
| app/Http/Requests/Api/V1/StoreCompanyRequest.php | Validation rules for creating companies via API. |
| app/Http/Middleware/SetApiTeamContext.php | Resolves/switches team (incl. X-Team-Id) and applies tenant scopes for API/MCP. |
| app/Http/Controllers/Api/V1/TasksController.php | Task CRUD endpoints delegating business logic to Actions. |
| app/Http/Controllers/Api/V1/PeopleController.php | People CRUD endpoints delegating business logic to Actions. |
| app/Http/Controllers/Api/V1/OpportunitiesController.php | Opportunity CRUD endpoints delegating business logic to Actions. |
| app/Http/Controllers/Api/V1/NotesController.php | Note CRUD endpoints delegating business logic to Actions. |
| app/Http/Controllers/Api/V1/CompaniesController.php | Company CRUD endpoints delegating business logic to Actions. |
| app/Filament/Resources/TaskResource.php | Switches Filament edit behavior to use shared UpdateTask Action. |
| app/Enums/CreationSource.php | Adds API creation source enum value for API/MCP created records. |
| app/Actions/Task/UpdateTask.php | Encapsulates task update + assignee notification behavior. |
| app/Actions/Task/ListTasks.php | Encapsulates task listing/filtering/pagination behavior. |
| app/Actions/Task/DeleteTask.php | Encapsulates task deletion behavior with policy enforcement. |
| app/Actions/Task/CreateTask.php | Encapsulates task creation behavior with source/team/creator tracking. |
| app/Actions/People/UpdatePeople.php | Encapsulates people update behavior with policy enforcement. |
| app/Actions/People/ListPeople.php | Encapsulates people listing/filtering/pagination behavior. |
| app/Actions/People/DeletePeople.php | Encapsulates people deletion behavior with policy enforcement. |
| app/Actions/People/CreatePeople.php | Encapsulates people creation behavior with source/team/creator tracking. |
| app/Actions/Opportunity/UpdateOpportunity.php | Encapsulates opportunity update behavior with policy enforcement. |
| app/Actions/Opportunity/ListOpportunities.php | Encapsulates opportunity listing/filtering/pagination behavior. |
| app/Actions/Opportunity/DeleteOpportunity.php | Encapsulates opportunity deletion behavior with policy enforcement. |
| app/Actions/Opportunity/CreateOpportunity.php | Encapsulates opportunity creation behavior with source/team/creator tracking. |
| app/Actions/Note/UpdateNote.php | Encapsulates note update behavior with policy enforcement. |
| app/Actions/Note/ListNotes.php | Encapsulates note listing/filtering/pagination behavior. |
| app/Actions/Note/DeleteNote.php | Encapsulates note deletion behavior with policy enforcement. |
| app/Actions/Note/CreateNote.php | Encapsulates note creation behavior with source/team/creator tracking. |
| app/Actions/Company/UpdateCompany.php | Encapsulates company update behavior with policy enforcement. |
| app/Actions/Company/ListCompanies.php | Encapsulates company listing/filtering/pagination behavior. |
| app/Actions/Company/DeleteCompany.php | Encapsulates company deletion behavior with policy enforcement. |
| app/Actions/Company/CreateCompany.php | Encapsulates company creation behavior with source/team/creator tracking. |
…g violation The dispatchFaviconFetchIfNeeded method loaded customFieldValues but not the nested customField relationship. When getValue() accessed customField->type, it triggered a LazyLoadingViolationException in non-production environments.
# Conflicts: # tests/ArchTest.php
…tests - Fix middleware priority: SetApiTeamContext runs before SubstituteBindings - Add Gate::authorize() to show() methods for defense-in-depth - Scope exists validation rules to prevent cross-tenant references - Remove duplicate throttle middleware from API routes - Return 201 status for store() endpoints - Integrate spatie/laravel-query-builder for declarative filtering/sorting - Add sort column allowlists via allowedSorts() - Update MCP tools for new List action signatures - Improve tests with fluent AssertableJson, assertInvalid(), whereType() - Add missing() assertions to verify no sensitive data leaks - Add cross-tenant isolation tests for all 5 resources - Simplify FormatsCustomFields with mapWithKeys() - Remove dead class_exists guard in UpdateTask
- Remove unnecessary parentheses around new Resource() calls in store methods - Use explicit instanceof Team check in SetApiTeamContext middleware - Add return type to arrow function in FormatsCustomFields trait
Note: Custom Fields Write Support (Next Step)This PR establishes the REST API foundation — CRUD, auth, team scoping, query builder, and tests. Custom fields are readable in responses but not yet writable via create/update endpoints. Plan for custom fields write support:
Dependency note:custom-fields#75 refactors the internal validation system (capabilities-based), but the public |
* feat: align opportunity view actions with people and companies (#138) Move Edit button outside the dropdown as a standalone action and add Copy Page URL / Copy Record ID actions inside the dropdown, matching the existing pattern in ViewPeople and ViewCompany. * feat: add ValidatesCustomFields trait for API FormRequests * feat: add custom field validation and write support to API - Wire ValidatesCustomFields trait into all 10 FormRequests - Extract and persist custom_fields in all Create/Update actions via TenantContextService - Change loadMissing to load in controllers for fresh custom field values - Add tests: create/update with custom fields, validation rejection, unknown fields ignored * feat: force JSON responses for all API routes * feat: add cursor pagination support to all list endpoints * feat: install and configure Scramble for auto-generated API docs * feat: add sparse fieldsets support to all list endpoints * refactor: accept explicit perPage parameter in List actions * feat: add custom fields metadata endpoint (GET /api/v1/custom-fields) * fix: resolve arch test failures and use correct model imports * fix: apply rector rules for type hints and return types * feat: enable API tokens feature in Jetstream * refactor: simplify MCP endpoint path from /mcp/relaticle to /mcp * feat: rebuild API tokens page with Filament components Replace raw Jetstream ApiTokenManager with custom Filament-based Livewire components matching the Profile page pattern: - CreateApiToken: form with token name (unique per user), required permissions, and modal displaying the new token with copy button - ManageApiTokens: Filament table with edit permissions and delete actions, proper wildcard abilities display - Fix rate-limited notification using missing translation keys - Add show() includes support for all 5 API controllers * style: fix pint formatting in API token components * fix: update laravel/mcp to v0.5.9 and fix tool pagination laravel/mcp v0.5.7 was missing the Server\Attributes directory (Description, Name, Version, etc.), causing tools/list to crash with "Attribute class not found" when clients tried to discover tools. Also increased default pagination from 15 to 50 so all 20 CRM tools are returned in a single page. * feat: migrate API resources to Laravel's JsonApiResource Adopt JSON:API format for all API responses using Laravel 12's native JsonApiResource. Resources now use toAttributes() and toRelationships() instead of toArray(), and controllers no longer need manual include parsing in show() methods since JsonApiResource handles ?include= automatically via loadMissing(). * test: update API tests for JSON:API response format Adapt all assertions to JSON:API structure: attributes nested under data.attributes, type field at data level, relationships and included for eager-loaded relations, and dot-notation plucks for collection data. * refactor: remove redundant foreign keys and add include tests Remove company_id/contact_id from resource attributes since they are redundant with declared JSON:API relationships. Clean up stale docblock on CustomFieldResource. Add include/relationship tests for all entities with deeper assertions on included entry structure. * feat: add team-scoped API tokens with expiration API tokens are now permanently scoped to a specific team at creation time, following the GitHub/Vercel pattern. This resolves issues where MCP tools hang because $user->currentTeam is null or wrong for programmatic clients. - Add team_id foreign key to personal_access_tokens table - Add team selector and expiration dropdown to token creation form - Show team and expiration columns in token management table - Resolve team from token first in SetApiTeamContext middleware - Create custom PersonalAccessToken model with team relationship
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 105 out of 108 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (2)
app/Livewire/App/ApiTokens/CreateApiToken.php:1
- The click handler uses direct JavaScript string interpolation with
$wire.plainTextToken, which could be vulnerable to XSS if the token contains unexpected characters. While Sanctum tokens are typically alphanumeric, consider using Livewire's native clipboard action or properly escaping the JavaScript context.
app/Http/Requests/Api/V1/Concerns/ValidatesCustomFields.php:1 - The
orWhereJsonContainswill match any field with a required rule, not scoped to the currentwhereclause context. This should be wrapped in a closure to ensure proper query grouping:$query->where(function($q) use ($submittedCodes) { $q->whereIn('code', $submittedCodes)->orWhereJsonContains(...); })
app/Filament/Resources/OpportunityResource/Pages/ViewOpportunity.php
Outdated
Show resolved
Hide resolved
app/Filament/Resources/OpportunityResource/Pages/ViewOpportunity.php
Outdated
Show resolved
Hide resolved
- Add terminate() to SetApiTeamContext for Octane-safe scope cleanup - Validate entity_type in CustomFieldsController with Rule::in() - Decouple UpdateTask from Filament panel context - Scope ForceJsonResponse middleware to v1 routes only - Use ??= in observers to prevent overwriting pre-set values - Add $fillable to PersonalAccessToken, exclude from final_class rule - Change CreationSource::API badge color from danger to info
Distinguish MCP-created records from REST API-created records by introducing a dedicated CreationSource::MCP case. All 5 MCP create tools now use this value instead of CreationSource::API.
database/migrations/2026_02_20_212853_add_team_id_to_personal_access_tokens_table.php
Show resolved
Hide resolved
Tokens are used for both REST API and MCP server connections. 'Access Tokens' better reflects the broader scope, following the pattern used by GitHub and Vercel.
Move all user-facing 'API Token' strings to lang/en/access-tokens.php and rename to 'Access Tokens' throughout. Covers Filament page, Livewire components, Blade templates, and user menu.
- Clamp per_page to minimum of 1 in all List actions to prevent invalid pagination - Use Js::from() for clipboard JS string escaping in View pages - Fix entity_type 'person' to 'people' in CustomFieldsApiTest to match morph map - Add PersonalAccessToken to arch test ignore list (Sanctum mocks prevent final)
…$request->integer() in API controllers
…thods for consistent authorization
…r creating() methods
…ureTokenHasAbility
Covers 5 targeted refactors from PR #136 code review: - Remove redundant Gate::authorize() from controllers - Move eager-loading into actions - Create IndexRequest for per_page validation - Fix FormatsCustomFields return type consistency
The ValidatesCustomFields trait called toRules() with raw request input during rule construction, before validation could reject non-array types. Sending custom_fields as a string or integer caused a TypeError crash (500) instead of a validation error (422). Guard with is_array() check and change nullable to sometimes.
Layered rate limiting: per-team ceiling (600/min), per-token reads (300/min) and writes (60/min). Currently the api/mcp rate limiters are defined but never applied.
6 tasks covering authorization cleanup, eager-loading, IndexRequest, FormatsCustomFields return type, and layered rate limiting.
…idation, rate limiting - Remove redundant Gate::authorize() from controllers (actions self-authorize) - Move customFieldValues eager-loading from controllers into actions - Create IndexRequest for per_page validation (replaces manual clamping) - Fix FormatsCustomFields to always return stdClass - Apply layered rate limiting: 600/min per team, 300/min reads, 60/min writes - Move throttle middleware after auth:sanctum for token-based keying
Phone, email, and link fields define per-item validation rules (phone:AUTO, email, url regex) via getItemValidationRules() but toRules() never included them. Invalid items in arrays passed validation silently.
…om fields Replace broken link tests that passed for the wrong reason (field didn't exist, so "unknown field" rejection matched). New tests create the custom field first, then verify item-level validation rules reject invalid values (bad domain, bad email, bad phone) and accept valid ones.
…essTokens - Eliminate ValidatesCustomFields traits (API + MCP) by consolidating custom field rule generation into ValidCustomFields::toRules() - Rename ApiTokens -> AccessTokens across classes, views, and tests to match user-facing "Access Tokens" terminology - Add @property-read Team|null $currentTeam to User model, removing stale @phpstan-ignore and redundant @var annotations - Remove dead code: redundant casts() in PersonalAccessToken, unused navigation properties in AccessTokens page, authorize() in ContactRequest, per_page clamping in CustomFieldsController
…closure return types
- Add section on using actions for write operations to ensure single-source-of-truth for business logic and side effects. - Include notes on Filament CRUD usage and best practices for extracting inline logic. - Add `laravel-best-practices` skill to guide adherence to Laravel coding patterns and refactoring standards.
…handling
- Sanitize top-level string fields (name, title) in HtmlSanitizer,
not just custom_fields, preventing stored XSS via API input
- Cache known custom field codes in ValidCustomFields to eliminate
duplicate DB query on every create/update request
- Replace allUsers()->pluck('id') with direct DB query in Task
requests to avoid hydrating full User models for ID collection
- Wrap NotifyTaskAssignees in defer() to send DB notifications
post-response instead of blocking the API caller
- Replace whereHas with whereIn subqueries in User tenant scope
for better index usage on team membership queries
- Add shouldRenderJsonWhen to bootstrap/app.php so exceptions
outside middleware pipeline still return JSON for API routes
- Validate team ownership on PersonalAccessToken updating event
(team_id is set post-creation via fill/save, bypassing creating)
… coverage
- Enforce read ability on MCP resources/prompts via native shouldRegister() hook
- Fix HtmlSanitizer XSS: recurse into nested array values in custom fields
- Add team-scoped Rule::exists() validation to MCP detach tools
- Redact available field list from custom field validation error messages
- Add null guard to TeamScope with whereRaw('1 = 0') to prevent data leaks
- Use $request->safe()->integer() for type-safe validated pagination params
- Make HtmlSanitizer and CustomFieldMerger readonly (fix arch test)
- Fix domains custom field unique constraint in CompaniesApiTest
- Exclude api/* from smoke tests (dedicated API test suite covers them)
- Remove redundant RefreshDatabase uses from 3 test files
- Add mutates() declarations, cursor pagination, PATCH, and unauth tests
…rs in AI agent preview - Reorder hero tabs: Pipeline > AI Agent > Companies > Custom Fields - Pipeline is now the default tab on page load - Add visible MCP tool call indicators (create-people-tool, list-opportunities-tool) to AI agent preview - Replace generic company names with distinctive ones (Kovra Systems, Meridian Health, Trellis Labs) - Redesign deal cards as a single unified card with divide-y rows for consistency - Add contact avatar with colored initials gradient - Fix animation flash by hiding elements via CSS before JS animation fires - Refine staggered animation sequence: avatar → tool call → text → card
Clean chat style matching Claude/ChatGPT — user messages are plain text, only agent responses show the sparkle avatar.
…view Add "You" and "Relaticle" labels with avatars to clearly show the conversation between user and AI agent, matching Claude/ChatGPT patterns.
Show ChatGPT as the AI assistant calling relaticle/ MCP tools, making it clear Relaticle is the tool provider — not a built-in chatbot.
The Symfony HtmlSanitizer was designed for rich HTML content but was applied to all string attributes including name/title fields. This caused data corruption: apostrophes became ', ampersands became &, and script tags produced empty names bypassing validation. Following Filament's standard approach: store raw data, escape on output. Blade {{ }} and Filament TextColumn handle XSS protection at render time. The API returns JSON which clients escape themselves.
Merge origin/main into feat/rest-api-mcp-server, keeping both: - API middleware priority (SetApiTeamContext) from feature branch - Guest redirect logic (team invitation flow) from main
Escape backslashes before % and _ to prevent double-escaping when input contains literal wildcard characters.
Summary
Full REST API (JSON:API format) and MCP server for all CRM entities, plus team-scoped API tokens with expiration.
REST API (v1)
JsonApiResourceForceJsonResponsemiddleware scoped to v1 routesSetApiTeamContextmiddleware with Octane-safe scope cleanupMCP Server
notablesupportTeam-Scoped API Tokens
team_idtopersonal_access_tokensPersonalAccessTokenmodel withteam()relationshipSecurity & Hardening
entity_typevalidation inCustomFieldsController??=to prevent overwriting pre-set values (API vs UI)UpdateTaskdecoupled from Filament panel contextCreationSource::APIbadge color changed from danger to infoTest Plan
Closes #85
Closes #89
Closes #133
Note
High Risk
Introduces new externally facing REST + MCP surfaces and changes Sanctum token scoping/ability enforcement via middleware, which is security- and multi-tenancy-critical and could cause authorization or data-leak regressions if misconfigured.
Overview
Adds a new
api/v1JSON:API REST surface for core CRM entities (companies/people/opportunities/tasks/notes) plus a custom-fields endpoint, with request validation, resource serialization, and list pagination/filtering wired through a shared Action layer.Introduces an MCP server (
RelaticleServer) exposing schema resources, a CRM overview prompt, and CRUD/list tools; refactors MCP tools around shared base classes and enforces token abilities for both API (newEnsureTokenHasAbilitymiddleware) and MCP tool execution.Hardens production readiness: makes
laravel/mcpa production dependency, scopes requests to teams viaSetApiTeamContext(Octane-safe cleanup and token/header team resolution), adds pagination to previously unbounded endpoints, adds short TTL caching for MCP schema/prompt reads, tightens mass-assignment viaArr::only()in create/update actions, and reduces/api/userexposure by returningUserResource.Expands test coverage substantially around token abilities, token team scoping and expiry, pagination/filtering/sorting parity across entities, MCP tool features, and token
team_idimmutability.Written by Cursor Bugbot for commit 8f9e27b. This will update automatically on new commits. Configure here.