-
Notifications
You must be signed in to change notification settings - Fork 913
Description
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
/mcpvia 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.descendantsafter Rails initialization - Mount FizzyMcpTransport middleware at
/mcppath prefix
lib/fizzy_mcp_transport.rb
- Inherit from
FastMcp::Transports::RackTransport - Override
handle_mcp_requestto validate Bearer tokens - Extract token from
HTTP_AUTHORIZATIONheader - Validate via
Identity::AccessToken.authenticate(token) - Set
Current.identityand resolve account (token.default_account or override) - Handle POST to
/mcpas 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_prefixcallback - 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_CONTEXTconstant describing Fizzy - Protected
#json_result(data)helper: wraps in MCP format{content: [{type: "text", text: data.to_json}], isError: false} - Protected
#current_user,#current_accountaccessors - 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_idparam orCurrent.identity.default_account - Delegate to existing
Search::Querywith user scope - Return array of results with type, id, title, snippet, URL
- Include HATEOAS hints for drilling down (use fizzy_read)
- Resolve account from
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::ChatCompletionandRaix::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
- Include
4. Raix LLM Assistant
app/services/fizzy_assistant.rb
- Include
Raix::ChatCompletion,Raix::FunctionDispatch - Include
FizzyToolFunctionsmodule (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_searchwith query, account_id, entity_type, limit paramsfunction :fizzy_readwith entity_type, identifier, account_id paramsfunction :fizzy_executewith command, account_id params
- Each function block:
- Validates account access (identity has user in account)
- Sets
Current.accountandCurrent.userviaCurrent.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:- OpenRouter configuration
- Manual test: POST to
/mcpwith invalid token → expect 401
Phase 2: Base Tool Infrastructure
- Create
app/tools/application_tool.rb:- Base class with
json_resulthelper, context accessors
- Base class with
- Create simple ping tool for testing:
app/tools/fizzy_ping_tool.rbreturns{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::Queryclass - Scope to
Current.user.accessible_cards/boards/users - Return array of results with snippets
- Use existing
- 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_toolandextract_tool_resulthelpers
- 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_executefunction
- Define
- 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_idoverride- 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::AccessTokendesign - Authorization enforced at runtime via
Userroles andaccessible_*scopes - Write tokens can perform all mutations user is authorized for
- Matches existing
-
Search: Use existing MySQL full-text search (no semantic embeddings)
- Leverages existing
Search::Querywith 16 shards - LLM interprets natural language → structured filters
- No new dependencies or indexing pipeline needed
- Can add semantic search in v2 if needed
- Leverages existing
Authorization Strategy
All operations respect Fizzy's existing authorization:
- Search/read use
Current.user.accessible_*associations (fromUser::Accessor) - Mutations check
User#can_administer_card?,User#can_administer_board? - Board access via
Accessrecords orall_accessflag - 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, 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 dispatch
Integration 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 cards
System Tests (optional):
- Use VCR to record LLM interactions
- Test full assistant conversation flow
Mock Strategy:
- Mock
Raix::ChatCompletionin 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 snippets4. 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, tags5. 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 number6. 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 gemsapp/models/identity/access_token.rb- Add MCP enhancements
Files 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.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?