Skip to content

Commit e8aeed8

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 e8aeed8

4 files changed

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

0 commit comments

Comments
 (0)