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
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,10 @@ The server supports configuration via environment variables with the `LFXMCP_` p
| `LFXMCP_CLIENT_ASSERTION_SIGNING_KEY` | PEM-encoded RSA private key for client assertion | - | No |
| `LFXMCP_TOKEN_ENDPOINT` | OAuth2 token endpoint URL for token exchange | - | No |
| `LFXMCP_LFX_API_URL` | LFX API URL (used as token exchange audience) | - | No |
| `LFXMCP_ONBOARDING_API_URL` | Base URL of the member onboarding service | - | No |
| `LFXMCP_ONBOARDING_API_AUDIENCE` | Auth0 resource server audience for the member onboarding API | - | No |
| `LFXMCP_LENS_API_URL` | Base URL of the LFX Lens service | - | No |
| `LFXMCP_LENS_API_AUDIENCE` | Auth0 resource server audience for the LFX Lens API | - | No |

**Example:**

Expand Down
98 changes: 97 additions & 1 deletion cmd/lfx-mcp-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/knadh/koanf/v2"
lfxauth "github.com/linuxfoundation/lfx-mcp/internal/auth"
"github.com/linuxfoundation/lfx-mcp/internal/lfxv2"
"github.com/linuxfoundation/lfx-mcp/internal/serviceapi"
"github.com/linuxfoundation/lfx-mcp/internal/tools"
"github.com/modelcontextprotocol/go-sdk/auth"
"github.com/modelcontextprotocol/go-sdk/mcp"
Expand All @@ -47,6 +48,12 @@ type Config struct {
// APICredentials is a consumer-key→shared-secret map for static API-key auth.
// TEMPORARY: stop-gap for MCP clients that cannot complete OAuth2.
APICredentials map[string]string `koanf:"api_credentials"`

// Service API configuration.
OnboardingAPIURL string `koanf:"onboarding_api_url"`
OnboardingAPIAudience string `koanf:"onboarding_api_audience"`
LensAPIURL string `koanf:"lens_api_url"`
LensAPIAudience string `koanf:"lens_api_audience"`
}

// HTTPConfig holds HTTP transport configuration.
Expand Down Expand Up @@ -112,6 +119,8 @@ var defaultTools = []string{
"get_past_meeting_transcript",
"search_past_meeting_summaries",
"get_past_meeting_summary",
"onboarding_list_memberships",
"lfx_lens_query",
}

var logger *slog.Logger
Expand Down Expand Up @@ -139,6 +148,10 @@ func main() {
f.String("tools", strings.Join(defaultTools, ","), "Comma-separated list of tools to enable")
f.Bool("debug", false, "Enable debug logging")
f.Bool("debug_traffic", false, "Enable HTTP request/response debug logging for outbound LFX API calls")
f.String("onboarding_api_url", "", "Base URL of the member onboarding service")
f.String("onboarding_api_audience", "", "Auth0 resource server audience for the member onboarding API")
f.String("lens_api_url", "", "Base URL of the LFX Lens service")
f.String("lens_api_audience", "", "Auth0 resource server audience for the LFX Lens API")

if err := f.Parse(os.Args[1:]); err != nil {
fmt.Fprintf(os.Stderr, "Failed to parse flags: %v\n", err)
Expand Down Expand Up @@ -270,6 +283,76 @@ func main() {
TokenExchangeClient: tokenExchangeClient,
DebugLogger: debugLogger,
})

// Configure service API infrastructure (shared across onboarding, lens, etc.).
slugResolver := lfxv2.NewSlugResolver()
accessChecker := lfxv2.NewAccessCheckClient(cfg.LFXAPIURL, &http.Client{Timeout: 30 * time.Second})

sharedAuth := tools.ServiceAuth{
LFXAPIURL: cfg.LFXAPIURL,
TokenExchangeClient: tokenExchangeClient,
DebugLogger: debugLogger,
SlugResolver: slugResolver,
AccessChecker: accessChecker,
}

// Shared M2M credentials for client credentials grants.
ccBase := lfxv2.ClientCredentialsConfig{
TokenEndpoint: cfg.TokenEndpoint,
ClientID: cfg.ClientID,
ClientSecret: cfg.ClientSecret,
ClientAssertionSigningKey: cfg.ClientAssertionSigningKey,
}

if cfg.OnboardingAPIURL != "" && cfg.OnboardingAPIAudience != "" {
ccCfg := ccBase
ccCfg.Audience = cfg.OnboardingAPIAudience
ccClient, err := lfxv2.NewClientCredentialsClient(ccCfg)
if err != nil {
logger.Warn("failed to create onboarding client credentials client", errKey, err)
} else {
onboardingClient, err := serviceapi.NewClient(serviceapi.Config{
BaseURL: cfg.OnboardingAPIURL,
TokenSource: ccClient,
HTTPClient: &http.Client{Timeout: 30 * time.Second},
DebugLogger: debugLogger,
})
if err != nil {
logger.Warn("failed to create onboarding service client", errKey, err)
} else {
tools.SetOnboardingConfig(&tools.OnboardingConfig{
ServiceAuth: sharedAuth,
ServiceClient: onboardingClient,
})
logger.Info("onboarding service tools configured", "url", cfg.OnboardingAPIURL)
}
}
}

if cfg.LensAPIURL != "" && cfg.LensAPIAudience != "" {
ccCfg := ccBase
ccCfg.Audience = cfg.LensAPIAudience
ccClient, err := lfxv2.NewClientCredentialsClient(ccCfg)
if err != nil {
logger.Warn("failed to create Lens client credentials client", errKey, err)
} else {
lensClient, err := serviceapi.NewClient(serviceapi.Config{
BaseURL: cfg.LensAPIURL,
TokenSource: ccClient,
HTTPClient: &http.Client{Timeout: 120 * time.Second},
DebugLogger: debugLogger,
})
if err != nil {
logger.Warn("failed to create Lens service client", errKey, err)
} else {
tools.SetLensConfig(&tools.LensConfig{
ServiceAuth: sharedAuth,
ServiceClient: lensClient,
})
logger.Info("LFX Lens tools configured", "url", cfg.LensAPIURL)
}
}
}
}
}

Expand Down Expand Up @@ -463,6 +546,14 @@ func newServer(cfg Config) *mcp.Server {
tools.RegisterGetPastMeetingSummary(server)
}

// Service API tools.
if enabledTools["onboarding_list_memberships"] {
tools.RegisterOnboardingListMemberships(server)
}
if enabledTools["lfx_lens_query"] {
tools.RegisterLFXLensQuery(server)
}

return server
}

Expand Down Expand Up @@ -581,10 +672,15 @@ func runHTTPServer(cfg Config) {
userID = "_anonymous"
}

// Store raw token in Extra for use in token exchange.
// Store raw token and custom claims in Extra for use by tool handlers.
extra := make(map[string]any)
extra["raw_token"] = tokenString

// Extract lf_staff custom claim for service tool authorization (LFX Lens).
if staffClaim, ok := token.Get(tools.ClaimLFStaff); ok {
extra[tools.ClaimLFStaff] = staffClaim
}

return &auth.TokenInfo{
UserID: userID,
Expiration: token.Expiration(),
Expand Down
Loading
Loading