Skip to content

Commit 5d9b048

Browse files
gpeden-lfCopilot
andcommitted
feat: add get_project_summary tool and parent_uid filter on search_projects
Add new get_project_summary tool that returns a rolled-up fact sheet for a project: child project count, committee count, working group count, meeting count, plus base metadata. First tool in the codebase to use QueryResourcesCount. Also adds parent_uid parameter to search_projects to support filtering child projects under a given foundation or umbrella project UID (ref ARCH-364). Submitting per Eric Searcy's recommendation given dev access blockers (LFXV2-1318) — LFX team to verify end-to-end. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: George Peden <gpeden@linuxfoundation.org>
1 parent 342898d commit 5d9b048

4 files changed

Lines changed: 202 additions & 6 deletions

File tree

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,11 @@ Before hitting **Connect**, follow the **Open Auth Settings** button, then selec
9797

9898
### Projects
9999

100-
| Tool | Description |
101-
|------|-------------|
102-
| `search_projects` | Search for LFX projects by name with typeahead and pagination |
103-
| `get_project` | Get a project's base info and settings by UID |
100+
| Tool | Description |
101+
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
102+
| `search_projects` | Search for LFX projects by name; optionally filter by parent project UID to list child projects |
103+
| `get_project` | Get a project's base info and settings by UID |
104+
| `get_project_summary` | Get a rolled-up fact sheet for a project: child project count, committee count, working group count, meeting count, plus base metadata |
104105

105106
### Committees
106107

cmd/lfx-mcp-server/main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ const errKey = "error"
7474
var defaultTools = []string{
7575
"search_projects",
7676
"get_project",
77+
"get_project_summary",
7778
"search_committees",
7879
"get_committee",
7980
"get_committee_member",
@@ -344,6 +345,9 @@ func newServer(cfg Config) *mcp.Server {
344345
if enabledTools["get_project"] {
345346
tools.RegisterGetProject(server)
346347
}
348+
if enabledTools["get_project_summary"] {
349+
tools.RegisterGetProjectSummary(server)
350+
}
347351
if enabledTools["search_committees"] {
348352
tools.RegisterSearchCommittees(server)
349353
}

internal/tools/project.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ func SetProjectConfig(cfg *ProjectConfig) {
3737
func RegisterSearchProjects(server *mcp.Server) {
3838
AddToolWithScopes(server, &mcp.Tool{
3939
Name: "search_projects",
40-
Description: "Search for LFX projects by name using the LFX query service",
40+
Description: "Search for LFX projects by name using the LFX query service. Optionally filter by parent project UID to get sub-projects.",
4141
Annotations: &mcp.ToolAnnotations{
4242
Title: "Search Projects",
4343
ReadOnlyHint: true,
@@ -59,7 +59,8 @@ func RegisterGetProject(server *mcp.Server) {
5959

6060
// SearchProjectsArgs defines the input parameters for the search_projects tool.
6161
type SearchProjectsArgs struct {
62-
Name string `json:"name" jsonschema:"Name or partial name of the project to search for"`
62+
Name string `json:"name,omitempty" jsonschema:"Name or partial name of the project to search for"`
63+
ParentUID string `json:"parent_uid,omitempty" jsonschema:"Optional parent project UID to filter sub-projects (e.g. get all children of a foundation)"`
6364
PageSize int `json:"page_size,omitempty" jsonschema:"Number of results per page (default 10, max 100)"`
6465
PageToken string `json:"page_token,omitempty" jsonschema:"Opaque pagination token from a previous search response"`
6566
}
@@ -128,6 +129,11 @@ func handleSearchProjects(ctx context.Context, req *mcp.CallToolRequest, args Se
128129
payload.Name = &args.Name
129130
}
130131

132+
if args.ParentUID != "" {
133+
parent := "project:" + args.ParentUID
134+
payload.Parent = &parent
135+
}
136+
131137
if args.PageToken != "" {
132138
payload.PageToken = &args.PageToken
133139
}

internal/tools/project_summary.go

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
// Copyright The Linux Foundation and contributors.
2+
// SPDX-License-Identifier: MIT
3+
4+
// Package tools provides MCP tool implementations for the LFX MCP server.
5+
package tools
6+
7+
import (
8+
"context"
9+
"encoding/json"
10+
"fmt"
11+
"log/slog"
12+
13+
"github.com/linuxfoundation/lfx-mcp/internal/lfxv2"
14+
projectservice "github.com/linuxfoundation/lfx-v2-project-service/api/project/v1/gen/project_service"
15+
querysvc "github.com/linuxfoundation/lfx-v2-query-service/gen/query_svc"
16+
"github.com/modelcontextprotocol/go-sdk/mcp"
17+
)
18+
19+
// RegisterGetProjectSummary registers the get_project_summary tool.
20+
func RegisterGetProjectSummary(server *mcp.Server) {
21+
AddToolWithScopes(server, &mcp.Tool{
22+
Name: "get_project_summary",
23+
Description: "Get a rolled-up fact sheet for an LFX project: child project count, " +
24+
"committee count, working group count, meeting count, plus base project metadata " +
25+
"(stage, legal entity type, formation date, funding model). " +
26+
"Useful for PMO complexity scoring and cost modeling.",
27+
Annotations: &mcp.ToolAnnotations{
28+
Title: "Get Project Summary",
29+
ReadOnlyHint: true,
30+
},
31+
}, ReadScopes(), handleGetProjectSummary)
32+
}
33+
34+
// GetProjectSummaryArgs defines input parameters for get_project_summary.
35+
type GetProjectSummaryArgs struct {
36+
UID string `json:"uid" jsonschema:"The v2 UID of the project to retrieve dimensions for"`
37+
}
38+
39+
// ProjectSummary is the rolled-up fact sheet returned by get_project_summary.
40+
type ProjectSummary struct {
41+
UID *string `json:"uid,omitempty"`
42+
Name *string `json:"name,omitempty"`
43+
Stage *string `json:"stage,omitempty"`
44+
LegalEntityType *string `json:"legal_entity_type,omitempty"`
45+
FormationDate *string `json:"formation_date,omitempty"`
46+
FundingModel []string `json:"funding_model,omitempty"`
47+
ChildProjectCount uint64 `json:"child_project_count"`
48+
CommitteeCount uint64 `json:"committee_count"`
49+
WorkingGroupCount uint64 `json:"working_group_count"`
50+
MeetingCount uint64 `json:"meeting_count"`
51+
}
52+
53+
// handleGetProjectSummary returns a rolled-up complexity fact sheet for a project.
54+
func handleGetProjectSummary(ctx context.Context, req *mcp.CallToolRequest, args GetProjectSummaryArgs) (*mcp.CallToolResult, any, error) {
55+
logger := slog.New(mcp.NewLoggingHandler(req.Session, nil))
56+
57+
if projectConfig == nil {
58+
return errorResult("project tools not configured"), nil, nil
59+
}
60+
61+
if args.UID == "" {
62+
return errorResult("uid is required"), nil, nil
63+
}
64+
65+
mcpToken, err := lfxv2.ExtractMCPToken(req.Extra.TokenInfo)
66+
if err != nil {
67+
return errorResult(fmt.Sprintf("failed to extract MCP token: %v", err)), nil, nil
68+
}
69+
70+
ctx = lfxv2.WithMCPToken(ctx, mcpToken)
71+
72+
clients, err := lfxv2.NewClients(ctx, lfxv2.ClientConfig{
73+
APIDomain: projectConfig.LFXAPIURL,
74+
TokenExchangeClient: projectConfig.TokenExchangeClient,
75+
DebugLogger: projectConfig.DebugLogger,
76+
})
77+
if err != nil {
78+
return errorResult(fmt.Sprintf("failed to connect to LFX API: %s", lfxv2.ErrorMessage(err))), nil, nil
79+
}
80+
81+
parent := "project:" + args.UID
82+
version := "1"
83+
84+
childType := projectResourceType
85+
commType := committeeResourceType
86+
mtgType := meetingResourceType
87+
88+
// Count child projects
89+
childCount, err := clients.QuerySvc.QueryResourcesCount(ctx, &querysvc.QueryResourcesCountPayload{
90+
Version: version,
91+
Type: &childType,
92+
Parent: &parent,
93+
})
94+
if err != nil {
95+
logger.Warn("failed to count child projects", "error", lfxv2.ErrorMessage(err))
96+
}
97+
98+
// Count all committees
99+
committeeCount, err := clients.QuerySvc.QueryResourcesCount(ctx, &querysvc.QueryResourcesCountPayload{
100+
Version: version,
101+
Type: &commType,
102+
Parent: &parent,
103+
})
104+
if err != nil {
105+
logger.Warn("failed to count committees", "error", lfxv2.ErrorMessage(err))
106+
}
107+
108+
// Count working groups (committees with category filter)
109+
wgCount, err := clients.QuerySvc.QueryResourcesCount(ctx, &querysvc.QueryResourcesCountPayload{
110+
Version: version,
111+
Type: &commType,
112+
Parent: &parent,
113+
Filters: []string{"category:Working Group"},
114+
})
115+
if err != nil {
116+
logger.Warn("failed to count working groups", "error", lfxv2.ErrorMessage(err))
117+
}
118+
119+
// Count meetings
120+
meetingCount, err := clients.QuerySvc.QueryResourcesCount(ctx, &querysvc.QueryResourcesCountPayload{
121+
Version: version,
122+
Type: &mtgType,
123+
Parent: &parent,
124+
})
125+
if err != nil {
126+
logger.Warn("failed to count meetings", "error", lfxv2.ErrorMessage(err))
127+
}
128+
129+
// Get base project info
130+
baseResult, err := clients.Project.GetOneProjectBase(ctx, &projectservice.GetOneProjectBasePayload{
131+
UID: &args.UID,
132+
})
133+
if err != nil {
134+
return errorResult(fmt.Sprintf("failed to get project: %s", lfxv2.ErrorMessage(err))), nil, nil
135+
}
136+
137+
p := baseResult.Project
138+
dims := ProjectSummary{
139+
UID: p.UID,
140+
Name: p.Name,
141+
Stage: p.Stage,
142+
LegalEntityType: p.LegalEntityType,
143+
FundingModel: p.FundingModel,
144+
FormationDate: p.FormationDate,
145+
}
146+
147+
if childCount != nil {
148+
dims.ChildProjectCount = childCount.Count
149+
}
150+
151+
if committeeCount != nil {
152+
dims.CommitteeCount = committeeCount.Count
153+
}
154+
155+
if wgCount != nil {
156+
dims.WorkingGroupCount = wgCount.Count
157+
}
158+
159+
if meetingCount != nil {
160+
dims.MeetingCount = meetingCount.Count
161+
}
162+
163+
prettyJSON, err := json.MarshalIndent(dims, "", " ")
164+
if err != nil {
165+
return errorResult(fmt.Sprintf("failed to format result: %v", err)), nil, nil
166+
}
167+
168+
logger.Info("get_project_summary succeeded", "uid", args.UID)
169+
170+
return &mcp.CallToolResult{
171+
Content: []mcp.Content{
172+
&mcp.TextContent{Text: string(prettyJSON)},
173+
},
174+
}, nil, nil
175+
}
176+
177+
// errorResult is a convenience helper for returning a tool error response.
178+
func errorResult(msg string) *mcp.CallToolResult {
179+
return &mcp.CallToolResult{
180+
Content: []mcp.Content{
181+
&mcp.TextContent{Text: "Error: " + msg},
182+
},
183+
IsError: true,
184+
}
185+
}

0 commit comments

Comments
 (0)