Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ sequenceDiagram
MCP->>Auth0: token exchange (RFC 8693)<br />M2M client assertion + {mcp_jwt}
Auth0-->>MCP: CTE token (carries user identity, cached)

MCP->>LFX: GET /committees?projectID={uuid}<br />Authorization: Bearer {cte_token}
MCP->>LFX: GET /committees/{uuid}<br />Authorization: Bearer {cte_token}
LFX->>LFX: verify token + OpenFGA authz<br />(natively, no MCP involvement)
LFX-->>MCP: committee data
MCP-->>Client: tool result
Expand Down Expand Up @@ -216,7 +216,7 @@ sequenceDiagram
Note over MCP: query_lfx_lens registered only when<br />read scope (read:all or manage:all) AND lf_staff=true
MCP-->>Client: tools/list (includes query_lfx_lens)

User->>Client: invoke query_lfx_lens (slug="tlf")
User->>Client: invoke query_lfx_lens (project_slug="tlf")
Client->>MCP: tools/call {query_lfx_lens}<br />Authorization: Bearer {mcp_jwt}

Note over MCP: lf_staff=true already verified at registration
Expand Down Expand Up @@ -256,7 +256,7 @@ sequenceDiagram
MCP->>MCP: verify signature, expiry, audience<br />extract scopes
MCP-->>Client: tools/list (filtered to caller's scopes)

User->>Client: invoke send_email (slug="tlf", ...)
User->>Client: invoke send_email (project_slug="tlf", ...)
Client->>MCP: tools/call {send_email}<br />Authorization: Bearer {mcp_jwt}

MCP->>Auth0: token exchange (RFC 8693)<br />M2M client assertion + {mcp_jwt}
Expand Down
50 changes: 27 additions & 23 deletions internal/tools/b2b_org.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,18 @@ func toB2bOrgMembershipView(m *memberservice.ProjectMembershipResponse) b2bOrgMe
}
}

// b2bOrgSearchResult is the output type for the search_b2b_orgs tool.
type b2bOrgSearchResult struct {
Orgs []b2bOrgView `json:"orgs"`
Metadata *memberservice.ListMetadata `json:"metadata,omitempty"`
}

// b2bOrgMembershipListResult is the output type for the list_b2b_org_memberships tool.
type b2bOrgMembershipListResult struct {
Memberships []b2bOrgMembershipView `json:"memberships"`
Metadata *memberservice.ListMetadata `json:"metadata,omitempty"`
}

// toB2bOrgView converts a B2bOrgResponse to the MCP view.
func toB2bOrgView(o *memberservice.B2bOrgResponse) b2bOrgView {
return b2bOrgView{
Expand Down Expand Up @@ -140,7 +152,7 @@ func RegisterListB2bOrgMemberships(server *mcp.Server) {
}

// handleSearchB2bOrgs implements the search_b2b_orgs tool logic.
func handleSearchB2bOrgs(ctx context.Context, req *mcp.CallToolRequest, args SearchB2bOrgsArgs) (*mcp.CallToolResult, any, error) {
func handleSearchB2bOrgs(ctx context.Context, req *mcp.CallToolRequest, args SearchB2bOrgsArgs) (*mcp.CallToolResult, b2bOrgSearchResult, error) {
logger := newToolLogger(ctx, req)

if memberConfig == nil {
Expand All @@ -150,7 +162,7 @@ func handleSearchB2bOrgs(ctx context.Context, req *mcp.CallToolRequest, args Sea
&mcp.TextContent{Text: "Error: member tools not configured"},
},
IsError: true,
}, nil, nil
}, b2bOrgSearchResult{}, nil
}

mcpToken, err := lfxv2.ExtractMCPToken(req.Extra.TokenInfo)
Expand All @@ -161,7 +173,7 @@ func handleSearchB2bOrgs(ctx context.Context, req *mcp.CallToolRequest, args Sea
&mcp.TextContent{Text: fmt.Sprintf("Error: failed to extract MCP token: %v", err)},
},
IsError: true,
}, nil, nil
}, b2bOrgSearchResult{}, nil
}

ctx = memberConfig.Clients.WithMCPToken(ctx, mcpToken)
Expand Down Expand Up @@ -198,19 +210,15 @@ func handleSearchB2bOrgs(ctx context.Context, req *mcp.CallToolRequest, args Sea
&mcp.TextContent{Text: friendlyAPIError("failed to search B2B orgs", err)},
},
IsError: true,
}, nil, nil
}, b2bOrgSearchResult{}, nil
}

views := make([]b2bOrgView, 0, len(result.Orgs))
for _, o := range result.Orgs {
views = append(views, toB2bOrgView(o))
}

type searchResult struct {
Orgs []b2bOrgView `json:"orgs"`
Metadata *memberservice.ListMetadata `json:"metadata,omitempty"`
}
output := searchResult{
output := b2bOrgSearchResult{
Orgs: views,
Metadata: result.Metadata,
}
Expand All @@ -223,7 +231,7 @@ func handleSearchB2bOrgs(ctx context.Context, req *mcp.CallToolRequest, args Sea
&mcp.TextContent{Text: fmt.Sprintf("Error: failed to format result: %v", err)},
},
IsError: true,
}, nil, nil
}, b2bOrgSearchResult{}, nil
}

logger.InfoContext(ctx, "search_b2b_orgs succeeded", "count", len(result.Orgs))
Expand All @@ -232,11 +240,11 @@ func handleSearchB2bOrgs(ctx context.Context, req *mcp.CallToolRequest, args Sea
Content: []mcp.Content{
&mcp.TextContent{Text: string(prettyJSON)},
},
}, nil, nil
}, output, nil
}

// handleListB2bOrgMemberships implements the list_b2b_org_memberships tool logic.
func handleListB2bOrgMemberships(ctx context.Context, req *mcp.CallToolRequest, args ListB2bOrgMembershipsArgs) (*mcp.CallToolResult, any, error) {
func handleListB2bOrgMemberships(ctx context.Context, req *mcp.CallToolRequest, args ListB2bOrgMembershipsArgs) (*mcp.CallToolResult, b2bOrgMembershipListResult, error) {
logger := newToolLogger(ctx, req)

if memberConfig == nil {
Expand All @@ -246,7 +254,7 @@ func handleListB2bOrgMemberships(ctx context.Context, req *mcp.CallToolRequest,
&mcp.TextContent{Text: "Error: member tools not configured"},
},
IsError: true,
}, nil, nil
}, b2bOrgMembershipListResult{}, nil
}

if args.B2bOrgUID == "" {
Expand All @@ -255,7 +263,7 @@ func handleListB2bOrgMemberships(ctx context.Context, req *mcp.CallToolRequest,
&mcp.TextContent{Text: "Error: b2b_org_uid is required"},
},
IsError: true,
}, nil, nil
}, b2bOrgMembershipListResult{}, nil
}

mcpToken, err := lfxv2.ExtractMCPToken(req.Extra.TokenInfo)
Expand All @@ -266,7 +274,7 @@ func handleListB2bOrgMemberships(ctx context.Context, req *mcp.CallToolRequest,
&mcp.TextContent{Text: fmt.Sprintf("Error: failed to extract MCP token: %v", err)},
},
IsError: true,
}, nil, nil
}, b2bOrgMembershipListResult{}, nil
}

ctx = memberConfig.Clients.WithMCPToken(ctx, mcpToken)
Expand Down Expand Up @@ -299,7 +307,7 @@ func handleListB2bOrgMemberships(ctx context.Context, req *mcp.CallToolRequest,
&mcp.TextContent{Text: friendlyAPIError("failed to list B2B org memberships", err)},
},
IsError: true,
}, nil, nil
}, b2bOrgMembershipListResult{}, nil
}

views := make([]b2bOrgMembershipView, 0, len(result.Memberships))
Expand All @@ -319,11 +327,7 @@ func handleListB2bOrgMemberships(ctx context.Context, req *mcp.CallToolRequest,
}
}

type listResult struct {
Memberships []b2bOrgMembershipView `json:"memberships"`
Metadata *memberservice.ListMetadata `json:"metadata,omitempty"`
}
output := listResult{
output := b2bOrgMembershipListResult{
Memberships: views,
Metadata: result.Metadata,
}
Expand All @@ -336,7 +340,7 @@ func handleListB2bOrgMemberships(ctx context.Context, req *mcp.CallToolRequest,
&mcp.TextContent{Text: fmt.Sprintf("Error: failed to format result: %v", err)},
},
IsError: true,
}, nil, nil
}, b2bOrgMembershipListResult{}, nil
}

logger.InfoContext(ctx, "list_b2b_org_memberships succeeded", "b2b_org_uid", args.B2bOrgUID, "count", len(result.Memberships))
Expand All @@ -346,5 +350,5 @@ func handleListB2bOrgMemberships(ctx context.Context, req *mcp.CallToolRequest,
content = append(content, &mcp.TextContent{Text: onboardingWarning})
}
content = append(content, &mcp.TextContent{Text: string(prettyJSON)})
return &mcp.CallToolResult{Content: content}, nil, nil
return &mcp.CallToolResult{Content: content}, output, nil
}
Loading
Loading