From c2f9311a37c22b23465b5c7556ae0960e7be91e9 Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Thu, 20 Nov 2025 18:37:28 +0100 Subject: [PATCH] Add listPeople tool Signed-off-by: Bryan Frimin --- pkg/server/api/mcp/v1/schema.resolvers.go | 29 +++++ pkg/server/api/mcp/v1/server/server.go | 11 ++ pkg/server/api/mcp/v1/specification.yaml | 138 +++++++++++++++++++++- pkg/server/api/mcp/v1/types/people.go | 53 +++++++++ pkg/server/api/mcp/v1/types/types.go | 64 +++++++++- 5 files changed, 291 insertions(+), 4 deletions(-) create mode 100644 pkg/server/api/mcp/v1/types/people.go diff --git a/pkg/server/api/mcp/v1/schema.resolvers.go b/pkg/server/api/mcp/v1/schema.resolvers.go index 6e7596850..62669b2d6 100644 --- a/pkg/server/api/mcp/v1/schema.resolvers.go +++ b/pkg/server/api/mcp/v1/schema.resolvers.go @@ -108,3 +108,32 @@ func (r *Resolver) AddVendorTool(ctx context.Context, req *mcp.CallToolRequest, func (r *Resolver) UpdateVendorTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateVendorInput) (*mcp.CallToolResult, types.UpdateVendorOutput, error) { return nil, types.UpdateVendorOutput{}, fmt.Errorf("updateVendor not implemented") } + +func (r *Resolver) ListPeopleTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListPeopleInput) (*mcp.CallToolResult, types.ListPeopleOutput, error) { + prb := r.ProboService(ctx, input.OrganizationID.TenantID()) + + pageOrderBy := page.OrderBy[coredata.PeopleOrderField]{ + Field: coredata.PeopleOrderFieldCreatedAt, + Direction: page.OrderDirectionDesc, + } + if input.OrderBy != nil { + pageOrderBy = page.OrderBy[coredata.PeopleOrderField]{ + Field: input.OrderBy.Field, + Direction: input.OrderBy.Direction, + } + } + + cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy) + + var peopleFilter = coredata.NewPeopleFilter(nil) + if input.Filter != nil { + peopleFilter = coredata.NewPeopleFilter(input.Filter.ExcludeContractEnded) + } + + page, err := prb.Peoples.ListForOrganizationID(ctx, input.OrganizationID, cursor, peopleFilter) + if err != nil { + panic(fmt.Errorf("cannot list organization people: %w", err)) + } + + return nil, types.NewListPeopleOutput(page), nil +} diff --git a/pkg/server/api/mcp/v1/server/server.go b/pkg/server/api/mcp/v1/server/server.go index 9bd835751..5e26bfbc2 100644 --- a/pkg/server/api/mcp/v1/server/server.go +++ b/pkg/server/api/mcp/v1/server/server.go @@ -12,6 +12,7 @@ import ( type ResolverInterface interface { ListOrganizationsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListOrganizationsInput) (*mcp.CallToolResult, types.ListOrganizationsOutput, error) ListVendorsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListVendorsInput) (*mcp.CallToolResult, types.ListVendorsOutput, error) + ListPeopleTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListPeopleInput) (*mcp.CallToolResult, types.ListPeopleOutput, error) AddVendorTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddVendorInput) (*mcp.CallToolResult, types.AddVendorOutput, error) UpdateVendorTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateVendorInput) (*mcp.CallToolResult, types.UpdateVendorOutput, error) } @@ -53,6 +54,16 @@ func registerToolHandlers(server *mcp.Server, resolver ResolverInterface) { }, resolver.ListVendorsTool, ) + mcp.AddTool( + server, + &mcp.Tool{ + Name: "listPeople", + Description: "List all people for the organization", + InputSchema: types.ListPeopleToolInputSchema, + OutputSchema: types.ListPeopleToolOutputSchema, + }, + resolver.ListPeopleTool, + ) mcp.AddTool( server, &mcp.Tool{ diff --git a/pkg/server/api/mcp/v1/specification.yaml b/pkg/server/api/mcp/v1/specification.yaml index 32e5c6227..70e1bb316 100644 --- a/pkg/server/api/mcp/v1/specification.yaml +++ b/pkg/server/api/mcp/v1/specification.yaml @@ -15,9 +15,9 @@ components: VendorOrderField: type: string enum: - - created_at - - updated_at - - name + - CREATED_AT + - UPDATED_AT + - NAME go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.VendorOrderField VendorOrderBy: @@ -32,6 +32,131 @@ components: direction: $ref: "#/components/schemas/OrderDirection" description: Vendor order direction + + PeopleOrderBy: + type: object + required: + - field + - direction + properties: + field: + $ref: "#/components/schemas/PeopleOrderField" + description: People order field + direction: + $ref: "#/components/schemas/OrderDirection" + description: People order direction + + PeopleOrderField: + type: string + enum: + - CREATED_AT + - FULL_NAME + - KIND + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.PeopleOrderField + + PeopleKind: + type: string + enum: + - EMPLOYEE + - CONTRACTOR + - SERVICE_ACCOUNT + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.PeopleKind + + People: + type: object + required: + - id + - organization_id + - full_name + - primary_email_address + - additional_email_addresses + - kind + - created_at + - updated_at + properties: + id: + $ref: "#/components/schemas/GID" + description: People ID + organization_id: + $ref: "#/components/schemas/GID" + description: Organization ID + full_name: + type: string + description: Full name + primary_email_address: + type: string + description: Primary email address + additional_email_addresses: + type: array + items: + type: string + description: Additional email addresses + kind: + $ref: "#/components/schemas/PeopleKind" + description: People kind + position: + type: + - string + - "null" + description: Position + contract_start_date: + type: + - string + - "null" + format: date-time + description: Contract start date + contract_end_date: + type: + - string + - "null" + format: date-time + description: Contract end date + created_at: + type: string + format: date-time + description: Creation timestamp + updated_at: + type: string + format: date-time + description: Update timestamp + + ListPeopleInput: + type: object + required: + - organization_id + properties: + organization_id: + $ref: "#/components/schemas/GID" + description: Organization ID + order_by: + $ref: "#/components/schemas/PeopleOrderBy" + description: People order by + size: + type: integer + description: Page size + cursor: + $ref: "#/components/schemas/CursorKey" + description: Page cursor + filter: + type: object + properties: + exclude_contract_ended: + type: boolean + description: Exclude people with ended contracts + + ListPeopleOutput: + type: object + required: + - people + properties: + next_cursor: + $ref: "#/components/schemas/CursorKey" + description: Next cursor + people: + type: array + items: + $ref: "#/components/schemas/People" + GID: type: string format: string @@ -225,6 +350,13 @@ tools: $ref: "#/components/schemas/ListVendorsInput" outputSchema: $ref: "#/components/schemas/ListVendorsOutput" + - name: listPeople + description: List all people for the organization + readonly: true + inputSchema: + $ref: "#/components/schemas/ListPeopleInput" + outputSchema: + $ref: "#/components/schemas/ListPeopleOutput" - name: addVendor description: Add a new vendor to the organization readonly: false diff --git a/pkg/server/api/mcp/v1/types/people.go b/pkg/server/api/mcp/v1/types/people.go new file mode 100644 index 000000000..8fe9a61f1 --- /dev/null +++ b/pkg/server/api/mcp/v1/types/people.go @@ -0,0 +1,53 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + +package types + +import ( + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/page" +) + +func NewPeople(p *coredata.People) *People { + return &People{ + FullName: p.FullName, + ID: p.ID, + OrganizationID: p.OrganizationID, + Kind: p.Kind, + PrimaryEmailAddress: p.PrimaryEmailAddress, + AdditionalEmailAddresses: p.AdditionalEmailAddresses, + Position: p.Position, + // ContractStartDate: p.ContractStartDate, + // ContractEndDate: p.ContractEndDate, + CreatedAt: p.CreatedAt, + UpdatedAt: p.UpdatedAt, + } +} + +func NewListPeopleOutput(peoplePage *page.Page[*coredata.People, coredata.PeopleOrderField]) ListPeopleOutput { + people := make([]*People, 0, len(peoplePage.Data)) + for _, p := range peoplePage.Data { + people = append(people, NewPeople(p)) + } + + var nextCursor *page.CursorKey + if len(peoplePage.Data) > 0 { + cursorKey := peoplePage.Data[len(peoplePage.Data)-1].CursorKey(peoplePage.Cursor.OrderBy.Field) + nextCursor = &cursorKey + } + + return ListPeopleOutput{ + NextCursor: nextCursor, + People: people, + } +} diff --git a/pkg/server/api/mcp/v1/types/types.go b/pkg/server/api/mcp/v1/types/types.go index 5213f2603..ef2049e4a 100644 --- a/pkg/server/api/mcp/v1/types/types.go +++ b/pkg/server/api/mcp/v1/types/types.go @@ -16,7 +16,9 @@ var ( AddVendorToolOutputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["vendor"],"properties":{"vendor":{"type":"object","required":["id","name","organization_id","created_at","updated_at"],"properties":{"created_at":{"type":"string","description":"Creation timestamp","format":"date-time"},"description":{"description":"Vendor description"},"id":{"type":"string","format":"string"},"name":{"type":"string","description":"Vendor name"},"organization_id":{"type":"string","format":"string"},"updated_at":{"type":"string","description":"Update timestamp","format":"date-time"}}}}}`) ListOrganizationsToolInputSchema = mcp.MustUnmarshalSchema(`{"type":"object","properties":{"organization_id":{"type":"string","format":"string"}}}`) ListOrganizationsToolOutputSchema = mcp.MustUnmarshalSchema(`{"type":"object","properties":{"organizations":{"type":"array","items":{"type":"object","required":["id","name","description","created_at","updated_at"],"properties":{"created_at":{"type":"string","description":"Creation timestamp","format":"date-time"},"description":{"description":"Organization description"},"id":{"type":"string","format":"string"},"name":{"type":"string","description":"Organization name"},"updated_at":{"type":"string","description":"Update timestamp","format":"date-time"}}}}}}`) - ListVendorsToolInputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["organization_id"],"properties":{"cursor":{"type":"string","format":"string"},"filter":{"type":"object","properties":{"snapshot_id":{"type":"string","format":"string"}}},"order_by":{"type":"object","required":["field","direction"],"properties":{"direction":{"type":"string","enum":["asc","desc"]},"field":{"type":"string","enum":["created_at","updated_at","name"]}}},"organization_id":{"type":"string","format":"string"},"size":{"type":"integer","description":"Page size"}}}`) + ListPeopleToolInputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["organization_id"],"properties":{"cursor":{"type":"string","format":"string"},"filter":{"type":"object","properties":{"exclude_contract_ended":{"type":"boolean","description":"Exclude people with ended contracts"}}},"order_by":{"type":"object","required":["field","direction"],"properties":{"direction":{"type":"string","enum":["asc","desc"]},"field":{"type":"string","enum":["CREATED_AT","FULL_NAME","KIND"]}}},"organization_id":{"type":"string","format":"string"},"size":{"type":"integer","description":"Page size"}}}`) + ListPeopleToolOutputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["people"],"properties":{"next_cursor":{"type":"string","format":"string"},"people":{"type":"array","items":{"type":"object","required":["id","organization_id","full_name","primary_email_address","additional_email_addresses","kind","created_at","updated_at"],"properties":{"additional_email_addresses":{"type":"array","description":"Additional email addresses","items":{"type":"string"}},"contract_end_date":{"description":"Contract end date","format":"date-time"},"contract_start_date":{"description":"Contract start date","format":"date-time"},"created_at":{"type":"string","description":"Creation timestamp","format":"date-time"},"full_name":{"type":"string","description":"Full name"},"id":{"type":"string","format":"string"},"kind":{"type":"string","enum":["EMPLOYEE","CONTRACTOR","SERVICE_ACCOUNT"]},"organization_id":{"type":"string","format":"string"},"position":{"description":"Position"},"primary_email_address":{"type":"string","description":"Primary email address"},"updated_at":{"type":"string","description":"Update timestamp","format":"date-time"}}}}}}`) + ListVendorsToolInputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["organization_id"],"properties":{"cursor":{"type":"string","format":"string"},"filter":{"type":"object","properties":{"snapshot_id":{"type":"string","format":"string"}}},"order_by":{"type":"object","required":["field","direction"],"properties":{"direction":{"type":"string","enum":["asc","desc"]},"field":{"type":"string","enum":["CREATED_AT","UPDATED_AT","NAME"]}}},"organization_id":{"type":"string","format":"string"},"size":{"type":"integer","description":"Page size"}}}`) ListVendorsToolOutputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["vendors"],"properties":{"next_cursor":{"type":"string","format":"string"},"vendors":{"type":"array","items":{"type":"object","required":["id","name","organization_id","created_at","updated_at"],"properties":{"created_at":{"type":"string","description":"Creation timestamp","format":"date-time"},"description":{"description":"Vendor description"},"id":{"type":"string","format":"string"},"name":{"type":"string","description":"Vendor name"},"organization_id":{"type":"string","format":"string"},"updated_at":{"type":"string","description":"Update timestamp","format":"date-time"}}}}}}`) UpdateVendorToolInputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["id"],"properties":{"description":{"type":"string","description":"Vendor description"},"id":{"type":"string","format":"string"},"name":{"type":"string","description":"Vendor name"}}}`) UpdateVendorToolOutputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["vendor"],"properties":{"vendor":{"type":"object","required":["id","name","organization_id","created_at","updated_at"],"properties":{"created_at":{"type":"string","description":"Creation timestamp","format":"date-time"},"description":{"description":"Vendor description"},"id":{"type":"string","format":"string"},"name":{"type":"string","description":"Vendor name"},"organization_id":{"type":"string","format":"string"},"updated_at":{"type":"string","description":"Update timestamp","format":"date-time"}}}}}`) @@ -52,6 +54,26 @@ type ListOrganizationsOutput struct { Organizations []*Organization `json:"organizations,omitempty"` } +// ListPeopleInput represents the schema +type ListPeopleInput struct { + // Page cursor + Cursor *page.CursorKey `json:"cursor,omitempty"` + Filter *ListPeopleInputFilter `json:"filter,omitempty"` + // People order by + OrderBy *PeopleOrderBy `json:"order_by,omitempty"` + // Organization ID + OrganizationID gid.GID `json:"organization_id"` + // Page size + Size *int64 `json:"size,omitempty"` +} + +// ListPeopleOutput represents the schema +type ListPeopleOutput struct { + // Next cursor + NextCursor *page.CursorKey `json:"next_cursor,omitempty"` + People []*People `json:"people"` +} + // ListVendorsInput represents the schema type ListVendorsInput struct { // Page cursor @@ -86,6 +108,40 @@ type Organization struct { UpdatedAt time.Time `json:"updated_at"` } +// People represents the schema +type People struct { + // Additional email addresses + AdditionalEmailAddresses []string `json:"additional_email_addresses"` + // Contract end date + ContractEndDate *string `json:"contract_end_date,omitempty"` + // Contract start date + ContractStartDate *string `json:"contract_start_date,omitempty"` + // Creation timestamp + CreatedAt time.Time `json:"created_at"` + // Full name + FullName string `json:"full_name"` + // People ID + ID gid.GID `json:"id"` + // People kind + Kind coredata.PeopleKind `json:"kind"` + // Organization ID + OrganizationID gid.GID `json:"organization_id"` + // Position + Position *string `json:"position,omitempty"` + // Primary email address + PrimaryEmailAddress string `json:"primary_email_address"` + // Update timestamp + UpdatedAt time.Time `json:"updated_at"` +} + +// PeopleOrderBy represents the schema +type PeopleOrderBy struct { + // People order direction + Direction page.OrderDirection `json:"direction"` + // People order field + Field coredata.PeopleOrderField `json:"field"` +} + // UpdateVendorInput represents the schema type UpdateVendorInput struct { // Vendor description @@ -125,6 +181,12 @@ type VendorOrderBy struct { Field coredata.VendorOrderField `json:"field"` } +// ListPeopleInputFilter represents the schema +type ListPeopleInputFilter struct { + // Exclude people with ended contracts + ExcludeContractEnded *bool `json:"exclude_contract_ended,omitempty"` +} + // ListVendorsInputFilter represents the schema type ListVendorsInputFilter struct { // Snapshot ID