Fizzy MCP Implementation Plan
Overview
Add an intelligent MCP HTTP endpoint to Fizzy that exposes 3 semantic tools (search, read, execute) powered by Raix + Gemini Flash 3. The LLM layer interprets plain language requests and maps them to internal Fizzy operations, avoiding the 44-tool explosion from fabric-pro/fizzy-mcp.
Architecture Summary
Pattern: Follow Nexus reference implementation
- MCP endpoint at
/mcp via fast-mcp gem + custom transport middleware
- Bearer token authentication via enhanced Identity::AccessToken model
- 3 MCP tools that delegate to Raix-powered assistant for intent interpretation
- Multi-tenancy via default account on token + optional override parameter
- Simple read/write permissions (no granular resource permissions)
- Use existing MySQL full-text search (no semantic embeddings needed)
Request Flow:
Client → POST /mcp (Bearer token)
→ FizzyMcpTransport validates token
→ FastMcp::Server routes to tool
→ Tool delegates to FizzyAssistant (Raix + Gemini Flash)
→ Assistant interprets plain language → function calls
→ Functions execute Fizzy operations in account context
→ Returns MCP-formatted JSON
Critical Files to Create
1. MCP Infrastructure
config/initializers/fast_mcp.rb
- Create FastMcp::Server instance (name: "fizzy", version: "1.0.0")
- Auto-register tools via
ApplicationTool.descendants after Rails initialization
- Mount FizzyMcpTransport middleware at
/mcp path prefix
lib/fizzy_mcp_transport.rb
- Inherit from
FastMcp::Transports::RackTransport
- Override
handle_mcp_request to validate Bearer tokens
- Extract token from
HTTP_AUTHORIZATION header
- Validate via
Identity::AccessToken.authenticate(token)
- Set
Current.identity and resolve account (token.default_account or override)
- Handle POST to
/mcp as messages endpoint (like Nexus)
- Use CaptureTransport pattern to return HTTP response (not SSE)
- Return 401 with OAuth URL if unauthorized
2. Enhanced Authentication Model
db/migrate/xxx_enhance_identity_access_tokens_for_mcp.rb
- Add columns:
name:string, prefix:string (indexed), expires_at:datetime, last_used_at:datetime, default_account_id:uuid
- Add foreign key:
default_account_id → accounts.id
- Generate prefix on creation (first 8 chars of token for fast lookup)
app/models/identity/access_token.rb (modify)
- Add
belongs_to :default_account, class_name: "Account", optional: true
- Add
before_create :set_prefix callback
- Add
.authenticate(token) class method (lookup by prefix, verify with BCrypt)
- Add
#expired? method
- Add
#touch_last_used! method
- Validate uniqueness of prefix
3. MCP Tools (3 tools)
app/tools/application_tool.rb
- Inherit from
ActionTool::Base
- Define
SERVER_CONTEXT constant describing Fizzy
- Protected
#json_result(data) helper: wraps in MCP format {content: [{type: "text", text: data.to_json}], isError: false}
- Protected
#current_user, #current_account accessors
- HATEOAS hint helpers:
#card_url_hint, #board_url_hint, etc.
app/tools/fizzy_search_tool.rb
- Description: "Find cards, boards, users by natural language query with filters"
- Arguments:
query:string (required), account_id:integer (optional), entity_type:string, limit:integer (default: 20)
- Implementation:
- Resolve account from
account_id param or Current.identity.default_account
- Delegate to existing
Search::Query with user scope
- Return array of results with type, id, title, snippet, URL
- Include HATEOAS hints for drilling down (use fizzy_read)
app/tools/fizzy_read_tool.rb
- Description: "Get specific entity by identifier (card number, board/user ID)"
- Arguments:
entity_type:string (card/board/user/comment), identifier:string, account_id:integer (optional)
- Implementation:
- Case switch on entity_type
- Card: find by number via
Current.user.accessible_cards.find_by!(number: identifier)
- Board: find via
Current.user.boards.find(identifier)
- User: find via
Current.account.users.find(identifier)
- Comment: find via
Current.user.accessible_comments.find(identifier)
- Return rich JSON (reuse JBuilder template patterns)
- Include related entity URLs, HATEOAS hints for actions
app/tools/fizzy_execute_tool.rb
- Description: "Perform actions via natural language: create, update, move, assign, tag, comment, close cards"
- Arguments:
command:string (required), account_id:integer (optional)
- Implementation:
- Include
Raix::ChatCompletion and Raix::FunctionDispatch
- Define internal functions for operations:
create_card(board_id, title, description, tags, assignee_ids)
update_card(card_number, title, description, column_id)
move_card(card_number, column_id:, board_id:)
assign_card(card_number, assignee_ids)
close_card(card_number, reason:)
add_comment(card_number, body)
add_tags(card_number, tag_titles)
- Use nested LLM to parse
command → function call
- Execute function in account context with authorization checks
- Return success/error with entity details
4. Raix LLM Assistant
app/services/fizzy_assistant.rb
- Include
Raix::ChatCompletion, Raix::FunctionDispatch
- Include
FizzyToolFunctions module (defines function bridges)
- System prompt: Describe Fizzy entities (cards, boards, users, tags, columns)
- Explain 3 available functions (fizzy_search, fizzy_read, fizzy_execute)
- Model:
ENV.fetch("FIZZY_QUERY_MODEL", "google/gemini-3-flash-preview")
#ask(query, account:, identity:, &streamer) method
- Store context (
@context = {account, identity}) for function calls
- Build transcript: system prompt + user query
- Call
chat_completion (Raix handles function dispatch)
app/services/concerns/fizzy_tool_functions.rb
- Define Raix function declarations for each of the 3 tools:
function :fizzy_search with query, account_id, entity_type, limit params
function :fizzy_read with entity_type, identifier, account_id params
function :fizzy_execute with command, account_id params
- Each function block:
- Validates account access (identity has user in account)
- Sets
Current.account and Current.user via Current.with_account
- Instantiates appropriate tool class
- Calls
tool.call(**args)
- Extracts result from MCP format (unwrap JSON string)
- Returns JSON string for LLM consumption
- Private
#call_tool(tool_class, **args) helper
- Private
#extract_tool_result(mcp_result) helper
5. Configuration
Gemfile (modify)
- Add
gem "fast_mcp"
- Add
gem "raix"
- Run
bundle install
config/initializers/raix.rb
- Configure Raix with OpenRouter client (like Nexus)
- Use
ENV["OPENROUTER_API_KEY"]
- Add retry logic and logging
Implementation Sequence
Phase 1: Foundation
- Add fast_mcp and raix to Gemfile
- Create and run AccessToken enhancement migration
- Modify
app/models/identity/access_token.rb:
- Add associations, callbacks, class methods
- Create
lib/fizzy_mcp_transport.rb:
- Token validation, account resolution, HTTP message handling
- Create
config/initializers/fast_mcp.rb:
- Server setup, middleware mounting
- Create
config/initializers/raix.rb:
- Manual test: POST to
/mcp with invalid token → expect 401
Phase 2: Base Tool Infrastructure
- Create
app/tools/application_tool.rb:
- Base class with
json_result helper, context accessors
- Create simple ping tool for testing:
app/tools/fizzy_ping_tool.rb returns {status: "ok", account: Current.account.name}
- Test tool registration works via
bin/rails runner "p ApplicationTool.descendants"
- Manual MCP test: Call ping tool, verify account context
Phase 3: Read-Only Tools
- Create
app/tools/fizzy_search_tool.rb:
- Use existing
Search::Query class
- Scope to
Current.user.accessible_cards/boards/users
- Return array of results with snippets
- Create
app/tools/fizzy_read_tool.rb:
- Case switch on entity_type
- Use
accessible_* associations for authorization
- Return rich JSON (mirror JBuilder structure)
- Add system tests for both tools
- Manual test via Claude Code MCP client
Phase 4: LLM Assistant Integration
- Create
app/services/concerns/fizzy_tool_functions.rb:
- Define Raix function declarations for search/read
- Implement
call_tool and extract_tool_result helpers
- Create
app/services/fizzy_assistant.rb:
- System prompt, model config, ask method
- Test assistant can interpret queries and call tools
- Integration test: Ask "find my urgent cards" → expect search call
Phase 5: Execute Tool (Mutations)
- Extend
app/tools/fizzy_execute_tool.rb:
- Define internal functions (create_card, move_card, etc.)
- Use nested Raix::FunctionDispatch for command parsing
- Authorization checks via
User#can_administer_card?
- Add function to
fizzy_tool_functions.rb:
- Define
fizzy_execute function
- Comprehensive tests:
- Create card with tags and assignees
- Move card between columns/boards
- Close card with reason comment
- Assign/unassign users
- Test authorization: member can't delete board
Phase 6: Polish & Documentation
- Error handling: Wrap tool calls in rescue blocks
- Add HATEOAS hints to all responses (URLs for related resources)
- Performance review: Check for N+1 queries, add includes
- README section on MCP usage:
- How to generate access token
- How to configure Claude Code
- Example queries
- API documentation for the 3 tools
Key Design Decisions (Based on User Input)
-
Multi-Tenancy: Default account on access token with optional account_id override
- Simplifies single-account use case
- Still supports multi-account identities
- Token creation UX must include account selection
-
Permissions: Simple read/write model (no granular per-resource permissions)
- Matches existing
Identity::AccessToken design
- Authorization enforced at runtime via
User roles and accessible_* scopes
- Write tokens can perform all mutations user is authorized for
-
Search: Use existing MySQL full-text search (no semantic embeddings)
- Leverages existing
Search::Query with 16 shards
- LLM interprets natural language → structured filters
- No new dependencies or indexing pipeline needed
- Can add semantic search in v2 if needed
Authorization Strategy
All operations respect Fizzy's existing authorization:
- Search/read use
Current.user.accessible_* associations (from User::Accessor)
- Mutations check
User#can_administer_card?, User#can_administer_board?
- Board access via
Access records or all_access flag
- Return 403 (not 404) when user lacks access to existing resource
- System role checks for dangerous operations (delete board, change settings)
Multi-Tenancy Implementation
- Token Creation: When creating access token, user selects default account
- Request Handling:
# In FizzyMcpTransport
account = if params[:account_id]
# Override: use specific account if provided
Account.find_by(external_account_id: params[:account_id])
else
# Default: use token's default account
token.default_account
end
# Validate identity has access
user = Current.identity.users.find_by(account: account)
raise Unauthorized unless user
# Set context
Current.with_account(account) do
Current.user = user
# ... execute tool
end
- Error Handling: Clear error if identity lacks user in requested account
Testing Strategy
Unit Tests:
test/models/identity/access_token_test.rb: Authentication, expiration
test/tools/fizzy_search_tool_test.rb: Search with various filters
test/tools/fizzy_read_tool_test.rb: Read each entity type
test/tools/fizzy_execute_tool_test.rb: Each mutation operation
test/services/fizzy_assistant_test.rb: Mock LLM, verify function dispatch
Integration Tests:
test/integration/mcp_authentication_test.rb: Token validation flow
test/integration/mcp_search_test.rb: End-to-end search via MCP
test/integration/mcp_mutations_test.rb: Create/update/move cards
System Tests (optional):
- Use VCR to record LLM interactions
- Test full assistant conversation flow
Mock Strategy:
- Mock
Raix::ChatCompletion in tests to avoid real API calls
- Provide fixture function call responses
- Test tool logic directly (bypass assistant layer)
Verification Plan (Manual Testing)
1. Setup
bin/rails db:migrate
bin/rails console
# Create access token with default account
identity = Identity.find_by(email: "david@example.com")
account = Account.first
token = identity.access_tokens.create!(
permission: :write,
name: "MCP Test Token",
default_account: account,
expires_at: 30.days.from_now
)
puts "Token: #{token.token}"
2. Test Authentication
curl -X POST http://fizzy.localhost:3006/mcp \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "tools/list",
"id": 1
}'
# Expect: List of 3 tools (fizzy_search, fizzy_read, fizzy_execute)
3. Test Search Tool
curl -X POST http://fizzy.localhost:3006/mcp \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "fizzy_search",
"arguments": {
"query": "urgent bugs",
"entity_type": "card",
"limit": 10
}
},
"id": 2
}'
# Expect: Array of matching cards with snippets
4. Test Read Tool
curl -X POST http://fizzy.localhost:3006/mcp \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "fizzy_read",
"arguments": {
"entity_type": "card",
"identifier": "1"
}
},
"id": 3
}'
# Expect: Full card details with comments, assignees, tags
5. Test Execute Tool (Create Card)
curl -X POST http://fizzy.localhost:3006/mcp \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "fizzy_execute",
"arguments": {
"command": "Create a card titled 'Test MCP Integration' on the first board"
}
},
"id": 4
}'
# Expect: Success with new card number
6. Test via Claude Code
- Configure Claude Code with MCP server:
{
"mcpServers": {
"fizzy": {
"url": "http://fizzy.localhost:3006/mcp",
"transport": "http",
"headers": {
"Authorization": "Bearer TOKEN"
}
}
}
}
- Ask Claude: "What urgent cards are assigned to me in Fizzy?"
- Verify: Claude calls fizzy_search → returns results
- Ask Claude: "Create a card about fixing the login bug"
- Verify: Claude calls fizzy_execute → card created
7. Test Multi-Account Override
# Get second account ID (external_account_id)
# Use account_id parameter to override default
curl -X POST http://fizzy.localhost:3006/mcp \
-H "Authorization: Bearer TOKEN" \
-d '{
"params": {
"name": "fizzy_search",
"arguments": {
"query": "bugs",
"account_id": 9876543
}
}
}'
# Expect: Results from second account (if identity has access)
8. Test Authorization Failures
- Try to read card from board without access → expect 403
- Try to delete board as member (not admin) → expect 403
- Try to use read token for mutations → expect 403
Files Modified
Gemfile - Add fast_mcp, raix gems
app/models/identity/access_token.rb - Add MCP enhancements
Files Created
db/migrate/xxx_enhance_identity_access_tokens_for_mcp.rb
lib/fizzy_mcp_transport.rb
config/initializers/fast_mcp.rb
config/initializers/raix.rb
app/tools/application_tool.rb
app/tools/fizzy_search_tool.rb
app/tools/fizzy_read_tool.rb
app/tools/fizzy_execute_tool.rb
app/services/fizzy_assistant.rb
app/services/concerns/fizzy_tool_functions.rb
- Test files for all of the above
Open Questions / Future Enhancements
- Rate Limiting: Add Rack::Attack before public beta?
- Token Management UI: Build admin interface for creating/revoking MCP tokens?
- Audit Logging: Track all MCP operations in Event stream?
- Semantic Search: Add pgvector for fuzzy queries in v2?
- Streaming Responses: Support SSE for long-running operations?
- Webhook Integration: Allow MCP to register webhooks for real-time updates?
Fizzy MCP Implementation Plan
Overview
Add an intelligent MCP HTTP endpoint to Fizzy that exposes 3 semantic tools (search, read, execute) powered by Raix + Gemini Flash 3. The LLM layer interprets plain language requests and maps them to internal Fizzy operations, avoiding the 44-tool explosion from fabric-pro/fizzy-mcp.
Architecture Summary
Pattern: Follow Nexus reference implementation
/mcpvia fast-mcp gem + custom transport middlewareRequest Flow:
Critical Files to Create
1. MCP Infrastructure
config/initializers/fast_mcp.rbApplicationTool.descendantsafter Rails initialization/mcppath prefixlib/fizzy_mcp_transport.rbFastMcp::Transports::RackTransporthandle_mcp_requestto validate Bearer tokensHTTP_AUTHORIZATIONheaderIdentity::AccessToken.authenticate(token)Current.identityand resolve account (token.default_account or override)/mcpas messages endpoint (like Nexus)2. Enhanced Authentication Model
db/migrate/xxx_enhance_identity_access_tokens_for_mcp.rbname:string,prefix:string(indexed),expires_at:datetime,last_used_at:datetime,default_account_id:uuiddefault_account_id→accounts.idapp/models/identity/access_token.rb(modify)belongs_to :default_account, class_name: "Account", optional: truebefore_create :set_prefixcallback.authenticate(token)class method (lookup by prefix, verify with BCrypt)#expired?method#touch_last_used!method3. MCP Tools (3 tools)
app/tools/application_tool.rbActionTool::BaseSERVER_CONTEXTconstant describing Fizzy#json_result(data)helper: wraps in MCP format{content: [{type: "text", text: data.to_json}], isError: false}#current_user,#current_accountaccessors#card_url_hint,#board_url_hint, etc.app/tools/fizzy_search_tool.rbquery:string(required),account_id:integer(optional),entity_type:string,limit:integer(default: 20)account_idparam orCurrent.identity.default_accountSearch::Querywith user scopeapp/tools/fizzy_read_tool.rbentity_type:string(card/board/user/comment),identifier:string,account_id:integer(optional)Current.user.accessible_cards.find_by!(number: identifier)Current.user.boards.find(identifier)Current.account.users.find(identifier)Current.user.accessible_comments.find(identifier)app/tools/fizzy_execute_tool.rbcommand:string(required),account_id:integer(optional)Raix::ChatCompletionandRaix::FunctionDispatchcreate_card(board_id, title, description, tags, assignee_ids)update_card(card_number, title, description, column_id)move_card(card_number, column_id:, board_id:)assign_card(card_number, assignee_ids)close_card(card_number, reason:)add_comment(card_number, body)add_tags(card_number, tag_titles)command→ function call4. Raix LLM Assistant
app/services/fizzy_assistant.rbRaix::ChatCompletion,Raix::FunctionDispatchFizzyToolFunctionsmodule (defines function bridges)ENV.fetch("FIZZY_QUERY_MODEL", "google/gemini-3-flash-preview")#ask(query, account:, identity:, &streamer)method@context = {account, identity}) for function callschat_completion(Raix handles function dispatch)app/services/concerns/fizzy_tool_functions.rbfunction :fizzy_searchwith query, account_id, entity_type, limit paramsfunction :fizzy_readwith entity_type, identifier, account_id paramsfunction :fizzy_executewith command, account_id paramsCurrent.accountandCurrent.userviaCurrent.with_accounttool.call(**args)#call_tool(tool_class, **args)helper#extract_tool_result(mcp_result)helper5. Configuration
Gemfile(modify)gem "fast_mcp"gem "raix"bundle installconfig/initializers/raix.rbENV["OPENROUTER_API_KEY"]Implementation Sequence
Phase 1: Foundation
app/models/identity/access_token.rb:lib/fizzy_mcp_transport.rb:config/initializers/fast_mcp.rb:config/initializers/raix.rb:/mcpwith invalid token → expect 401Phase 2: Base Tool Infrastructure
app/tools/application_tool.rb:json_resulthelper, context accessorsapp/tools/fizzy_ping_tool.rbreturns{status: "ok", account: Current.account.name}bin/rails runner "p ApplicationTool.descendants"Phase 3: Read-Only Tools
app/tools/fizzy_search_tool.rb:Search::QueryclassCurrent.user.accessible_cards/boards/usersapp/tools/fizzy_read_tool.rb:accessible_*associations for authorizationPhase 4: LLM Assistant Integration
app/services/concerns/fizzy_tool_functions.rb:call_toolandextract_tool_resulthelpersapp/services/fizzy_assistant.rb:Phase 5: Execute Tool (Mutations)
app/tools/fizzy_execute_tool.rb:User#can_administer_card?fizzy_tool_functions.rb:fizzy_executefunctionPhase 6: Polish & Documentation
Key Design Decisions (Based on User Input)
Multi-Tenancy: Default account on access token with optional
account_idoverridePermissions: Simple read/write model (no granular per-resource permissions)
Identity::AccessTokendesignUserroles andaccessible_*scopesSearch: Use existing MySQL full-text search (no semantic embeddings)
Search::Querywith 16 shardsAuthorization Strategy
All operations respect Fizzy's existing authorization:
Current.user.accessible_*associations (fromUser::Accessor)User#can_administer_card?,User#can_administer_board?Accessrecords orall_accessflagMulti-Tenancy Implementation
Testing Strategy
Unit Tests:
test/models/identity/access_token_test.rb: Authentication, expirationtest/tools/fizzy_search_tool_test.rb: Search with various filterstest/tools/fizzy_read_tool_test.rb: Read each entity typetest/tools/fizzy_execute_tool_test.rb: Each mutation operationtest/services/fizzy_assistant_test.rb: Mock LLM, verify function dispatchIntegration Tests:
test/integration/mcp_authentication_test.rb: Token validation flowtest/integration/mcp_search_test.rb: End-to-end search via MCPtest/integration/mcp_mutations_test.rb: Create/update/move cardsSystem Tests (optional):
Mock Strategy:
Raix::ChatCompletionin tests to avoid real API callsVerification Plan (Manual Testing)
1. Setup
2. Test Authentication
3. Test Search Tool
4. Test Read Tool
5. Test Execute Tool (Create Card)
6. Test via Claude Code
{ "mcpServers": { "fizzy": { "url": "http://fizzy.localhost:3006/mcp", "transport": "http", "headers": { "Authorization": "Bearer TOKEN" } } } }7. Test Multi-Account Override
8. Test Authorization Failures
Files Modified
Gemfile- Add fast_mcp, raix gemsapp/models/identity/access_token.rb- Add MCP enhancementsFiles Created
db/migrate/xxx_enhance_identity_access_tokens_for_mcp.rblib/fizzy_mcp_transport.rbconfig/initializers/fast_mcp.rbconfig/initializers/raix.rbapp/tools/application_tool.rbapp/tools/fizzy_search_tool.rbapp/tools/fizzy_read_tool.rbapp/tools/fizzy_execute_tool.rbapp/services/fizzy_assistant.rbapp/services/concerns/fizzy_tool_functions.rbOpen Questions / Future Enhancements