From fa97a8e9c6a6e13654d1d99bbe5c26e928baab97 Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Sun, 23 Nov 2025 20:58:23 +0100 Subject: [PATCH] Add risks Signed-off-by: Bryan Frimin --- go.mod | 2 +- go.sum | 4 +- pkg/server/api/mcp/v1/schema.resolvers.go | 100 +++++++ pkg/server/api/mcp/v1/server/server.go | 44 ++++ pkg/server/api/mcp/v1/specification.yaml | 302 ++++++++++++++++++++++ pkg/server/api/mcp/v1/types/cursorkey.go | 2 +- pkg/server/api/mcp/v1/types/risk.go | 50 ++++ pkg/server/api/mcp/v1/types/types.go | 159 +++++++++++- pkg/server/api/mcp/v1/v1_handler.go | 9 + 9 files changed, 666 insertions(+), 6 deletions(-) create mode 100644 pkg/server/api/mcp/v1/types/risk.go diff --git a/go.mod b/go.mod index 8d0320d6d..0e414c823 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( go.gearno.de/x/ref v0.0.0-20240502200927-d74926fcb14c go.opentelemetry.io/otel v1.38.0 go.opentelemetry.io/otel/trace v1.38.0 - go.probo.inc/mcpgen v0.0.0-20251120181856-7af25b57c072 + go.probo.inc/mcpgen v0.0.0-20251123193256-f77e7775c058 golang.org/x/crypto v0.45.0 golang.org/x/image v0.33.0 golang.org/x/sync v0.18.0 diff --git a/go.sum b/go.sum index de9d5a21b..c3bd0da35 100644 --- a/go.sum +++ b/go.sum @@ -245,8 +245,8 @@ go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJr go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= -go.probo.inc/mcpgen v0.0.0-20251120181856-7af25b57c072 h1:cDcrtdjfFK6bOC/feYvSRxFH2w89ZvbrdWmywHpNfEo= -go.probo.inc/mcpgen v0.0.0-20251120181856-7af25b57c072/go.mod h1:HunWQGqLdMocExJh4tWaX7p+uRZ9GlKvBvOXHaFW6vM= +go.probo.inc/mcpgen v0.0.0-20251123193256-f77e7775c058 h1:zRiWKVVJZCDDolmVedKm0xdQlJQXvLzgFEDu/s20jYs= +go.probo.inc/mcpgen v0.0.0-20251123193256-f77e7775c058/go.mod h1:HunWQGqLdMocExJh4tWaX7p+uRZ9GlKvBvOXHaFW6vM= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= diff --git a/pkg/server/api/mcp/v1/schema.resolvers.go b/pkg/server/api/mcp/v1/schema.resolvers.go index ea52f53cc..c02f46fb8 100644 --- a/pkg/server/api/mcp/v1/schema.resolvers.go +++ b/pkg/server/api/mcp/v1/schema.resolvers.go @@ -192,3 +192,103 @@ func (r *Resolver) AddPeopleTool(ctx context.Context, req *mcp.CallToolRequest, People: types.NewPeople(people), }, nil } + +func (r *Resolver) ListRisksTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListRisksInput) (*mcp.CallToolResult, types.ListRisksOutput, error) { + r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionListRisks) + + prb := r.ProboService(ctx, input.OrganizationID) + + pageOrderBy := page.OrderBy[coredata.RiskOrderField]{ + Field: coredata.RiskOrderFieldCreatedAt, + Direction: page.OrderDirectionDesc, + } + if input.OrderBy != nil { + pageOrderBy = page.OrderBy[coredata.RiskOrderField]{ + Field: input.OrderBy.Field, + Direction: input.OrderBy.Direction, + } + } + + cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy) + + var riskFilter = coredata.NewRiskFilter(nil, nil) + if input.Filter != nil { + riskFilter = coredata.NewRiskFilter(input.Filter.Query, &input.Filter.SnapshotID) + } + + page, err := prb.Risks.ListForOrganizationID(ctx, input.OrganizationID, cursor, riskFilter) + if err != nil { + panic(fmt.Errorf("cannot list organization risks: %w", err)) + } + + return nil, types.NewListRisksOutput(page), nil +} +func (r *Resolver) GetRiskTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetRiskInput) (*mcp.CallToolResult, types.GetRiskOutput, error) { + r.MustBeAuthorized(ctx, input.ID, authz.ActionGet) + + prb := r.ProboService(ctx, input.ID) + + risk, err := prb.Risks.Get(ctx, input.ID) + if err != nil { + return nil, types.GetRiskOutput{}, fmt.Errorf("failed to get risk: %w", err) + } + + return nil, types.GetRiskOutput{ + Risk: types.NewRisk(risk), + }, nil +} +func (r *Resolver) AddRiskTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddRiskInput) (*mcp.CallToolResult, types.AddRiskOutput, error) { + r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionCreateRisk) + + svc := r.ProboService(ctx, input.OrganizationID) + + risk, err := svc.Risks.Create( + ctx, + probo.CreateRiskRequest{ + OrganizationID: input.OrganizationID, + Name: input.Name, + Description: input.Description, + Category: input.Category, + Treatment: input.Treatment, + InherentLikelihood: input.InherentLikelihood, + InherentImpact: input.InherentImpact, + ResidualLikelihood: input.ResidualLikelihood, + ResidualImpact: input.ResidualImpact, + }, + ) + if err != nil { + return nil, types.AddRiskOutput{}, fmt.Errorf("failed to create risk: %w", err) + } + + return nil, types.AddRiskOutput{ + Risk: types.NewRisk(risk), + }, nil +} +func (r *Resolver) UpdateRiskTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateRiskInput) (*mcp.CallToolResult, types.UpdateRiskOutput, error) { + r.MustBeAuthorized(ctx, input.ID, authz.ActionUpdateRisk) + + svc := r.ProboService(ctx, input.ID) + + risk, err := svc.Risks.Update( + ctx, + probo.UpdateRiskRequest{ + ID: input.ID, + Name: input.Name, + Description: UnwrapOmittable(input.Description), + Category: input.Category, + Treatment: input.Treatment, + OwnerID: UnwrapOmittable(input.OwnerID), + InherentLikelihood: input.InherentLikelihood, + InherentImpact: input.InherentImpact, + ResidualLikelihood: input.ResidualLikelihood, + ResidualImpact: input.ResidualImpact, + }, + ) + if err != nil { + return nil, types.UpdateRiskOutput{}, fmt.Errorf("failed to update risk: %w", err) + } + + return nil, types.UpdateRiskOutput{ + Risk: types.NewRisk(risk), + }, nil +} diff --git a/pkg/server/api/mcp/v1/server/server.go b/pkg/server/api/mcp/v1/server/server.go index 8d1c03b0c..869948834 100644 --- a/pkg/server/api/mcp/v1/server/server.go +++ b/pkg/server/api/mcp/v1/server/server.go @@ -17,6 +17,10 @@ type ResolverInterface interface { UpdateVendorTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateVendorInput) (*mcp.CallToolResult, types.UpdateVendorOutput, error) GetPeopleTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetPeopleInput) (*mcp.CallToolResult, types.GetPeopleOutput, error) AddPeopleTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddPeopleInput) (*mcp.CallToolResult, types.AddPeopleOutput, error) + ListRisksTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListRisksInput) (*mcp.CallToolResult, types.ListRisksOutput, error) + GetRiskTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetRiskInput) (*mcp.CallToolResult, types.GetRiskOutput, error) + AddRiskTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddRiskInput) (*mcp.CallToolResult, types.AddRiskOutput, error) + UpdateRiskTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateRiskInput) (*mcp.CallToolResult, types.UpdateRiskOutput, error) } // New creates a new MCP server instance with all handlers registered. @@ -106,4 +110,44 @@ func registerToolHandlers(server *mcp.Server, resolver ResolverInterface) { }, resolver.AddPeopleTool, ) + mcp.AddTool( + server, + &mcp.Tool{ + Name: "listRisks", + Description: "List all risks for the organization", + InputSchema: types.ListRisksToolInputSchema, + OutputSchema: types.ListRisksToolOutputSchema, + }, + resolver.ListRisksTool, + ) + mcp.AddTool( + server, + &mcp.Tool{ + Name: "getRisk", + Description: "Get a risk by ID", + InputSchema: types.GetRiskToolInputSchema, + OutputSchema: types.GetRiskToolOutputSchema, + }, + resolver.GetRiskTool, + ) + mcp.AddTool( + server, + &mcp.Tool{ + Name: "addRisk", + Description: "Add a new risk to the organization", + InputSchema: types.AddRiskToolInputSchema, + OutputSchema: types.AddRiskToolOutputSchema, + }, + resolver.AddRiskTool, + ) + mcp.AddTool( + server, + &mcp.Tool{ + Name: "updateRisk", + Description: "Update an existing risk", + InputSchema: types.UpdateRiskToolInputSchema, + OutputSchema: types.UpdateRiskToolOutputSchema, + }, + resolver.UpdateRiskTool, + ) } diff --git a/pkg/server/api/mcp/v1/specification.yaml b/pkg/server/api/mcp/v1/specification.yaml index 807eb58a0..0111fa28b 100644 --- a/pkg/server/api/mcp/v1/specification.yaml +++ b/pkg/server/api/mcp/v1/specification.yaml @@ -397,6 +397,280 @@ components: people: $ref: "#/components/schemas/People" + RiskOrderField: + type: string + enum: + - CREATED_AT + - UPDATED_AT + - NAME + - CATEGORY + - TREATMENT + - INHERENT_RISK_SCORE + - RESIDUAL_RISK_SCORE + - OWNER_FULL_NAME + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.RiskOrderField + + RiskOrderBy: + type: object + required: + - field + - direction + properties: + field: + $ref: "#/components/schemas/RiskOrderField" + description: Risk order field + direction: + $ref: "#/components/schemas/OrderDirection" + description: Risk order direction + + RiskTreatment: + type: string + enum: + - MITIGATED + - ACCEPTED + - AVOIDED + - TRANSFERRED + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.RiskTreatment + + Risk: + type: object + required: + - id + - organization_id + - name + - category + - treatment + - inherent_likelihood + - inherent_impact + - inherent_risk_score + - residual_likelihood + - residual_impact + - residual_risk_score + - note + - created_at + - updated_at + properties: + id: + $ref: "#/components/schemas/GID" + description: Risk ID + organization_id: + $ref: "#/components/schemas/GID" + description: Organization ID + snapshot_id: + type: + - string + - "null" + description: Snapshot ID + name: + type: string + description: Risk name + description: + type: + - string + - "null" + description: Risk description + category: + type: string + description: Risk category + treatment: + $ref: "#/components/schemas/RiskTreatment" + description: Risk treatment + inherent_likelihood: + type: integer + description: Inherent likelihood + inherent_impact: + type: integer + description: Inherent impact + inherent_risk_score: + type: integer + description: Inherent risk score + residual_likelihood: + type: integer + description: Residual likelihood + residual_impact: + type: integer + description: Residual impact + residual_risk_score: + type: integer + description: Residual risk score + note: + type: string + description: Risk note + owner_id: + type: + - string + - "null" + description: Owner ID + created_at: + type: string + format: date-time + description: Creation timestamp + updated_at: + type: string + format: date-time + description: Update timestamp + + ListRisksInput: + type: object + required: + - organization_id + properties: + organization_id: + $ref: "#/components/schemas/GID" + description: Organization ID + order_by: + $ref: "#/components/schemas/RiskOrderBy" + description: Risk order by + size: + type: integer + description: Page size + cursor: + $ref: "#/components/schemas/CursorKey" + description: Page cursor + filter: + type: object + properties: + query: + type: string + description: Search query + snapshot_id: + $ref: "#/components/schemas/GID" + description: Snapshot ID + + ListRisksOutput: + type: object + required: + - risks + properties: + next_cursor: + $ref: "#/components/schemas/CursorKey" + description: Next cursor + risks: + type: array + items: + $ref: "#/components/schemas/Risk" + + GetRiskInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Risk ID + + GetRiskOutput: + type: object + required: + - risk + properties: + risk: + $ref: "#/components/schemas/Risk" + + AddRiskInput: + type: object + required: + - organization_id + - name + - category + - treatment + - inherent_likelihood + - inherent_impact + properties: + organization_id: + $ref: "#/components/schemas/GID" + description: Organization ID + name: + type: string + description: Risk name + description: + type: string + description: Risk description + category: + type: string + description: Risk category + owner_id: + $ref: "#/components/schemas/GID" + description: Owner ID + treatment: + $ref: "#/components/schemas/RiskTreatment" + description: Risk treatment + inherent_likelihood: + type: integer + description: Inherent likelihood + inherent_impact: + type: integer + description: Inherent impact + residual_likelihood: + type: integer + description: Residual likelihood + residual_impact: + type: integer + description: Residual impact + note: + type: string + description: Risk note + + AddRiskOutput: + type: object + required: + - risk + properties: + risk: + $ref: "#/components/schemas/Risk" + + UpdateRiskInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Risk ID + name: + type: string + description: Risk name + description: + type: ["string", "null"] + description: Risk description + go.probo.inc/mcpgen/omittable: true + category: + type: string + description: Risk category + owner_id: + anyOf: + - type: string + $ref: "#/components/schemas/GID" + - type: "null" + description: Owner ID + go.probo.inc/mcpgen/omittable: true + treatment: + $ref: "#/components/schemas/RiskTreatment" + description: Risk treatment + inherent_likelihood: + type: integer + description: Inherent likelihood + inherent_impact: + type: integer + description: Inherent impact + residual_likelihood: + type: integer + description: Residual likelihood + residual_impact: + type: integer + description: Residual impact + note: + type: string + description: Risk note + + UpdateRiskOutput: + type: object + required: + - risk + properties: + risk: + $ref: "#/components/schemas/Risk" + tools: - name: listOrganizations description: List all organizations the user has access to @@ -447,3 +721,31 @@ tools: $ref: "#/components/schemas/AddPeopleInput" outputSchema: $ref: "#/components/schemas/AddPeopleOutput" + - name: listRisks + description: List all risks for the organization + readonly: true + inputSchema: + $ref: "#/components/schemas/ListRisksInput" + outputSchema: + $ref: "#/components/schemas/ListRisksOutput" + - name: getRisk + description: Get a risk by ID + readonly: true + inputSchema: + $ref: "#/components/schemas/GetRiskInput" + outputSchema: + $ref: "#/components/schemas/GetRiskOutput" + - name: addRisk + description: Add a new risk to the organization + readonly: false + inputSchema: + $ref: "#/components/schemas/AddRiskInput" + outputSchema: + $ref: "#/components/schemas/AddRiskOutput" + - name: updateRisk + description: Update an existing risk + readonly: false + inputSchema: + $ref: "#/components/schemas/UpdateRiskInput" + outputSchema: + $ref: "#/components/schemas/UpdateRiskOutput" diff --git a/pkg/server/api/mcp/v1/types/cursorkey.go b/pkg/server/api/mcp/v1/types/cursorkey.go index ce7fd01ae..002b48169 100644 --- a/pkg/server/api/mcp/v1/types/cursorkey.go +++ b/pkg/server/api/mcp/v1/types/cursorkey.go @@ -21,7 +21,7 @@ import ( ) func NewCursor[O page.OrderField]( - first *int64, + first *int, after *page.CursorKey, orderBy page.OrderBy[O], ) *page.Cursor[O] { diff --git a/pkg/server/api/mcp/v1/types/risk.go b/pkg/server/api/mcp/v1/types/risk.go new file mode 100644 index 000000000..075034211 --- /dev/null +++ b/pkg/server/api/mcp/v1/types/risk.go @@ -0,0 +1,50 @@ +// 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 +// PERFORMANCE OF THIS SOFTWARE. + +package types + +import ( + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/page" +) + +func NewRisk(r *coredata.Risk) *Risk { + return &Risk{ + ID: r.ID, + Name: r.Name, + Description: r.Description, + Category: r.Category, + Treatment: r.Treatment, + InherentLikelihood: r.InherentLikelihood, + InherentImpact: r.InherentImpact, + } +} + +func NewListRisksOutput(riskPage *page.Page[*coredata.Risk, coredata.RiskOrderField]) ListRisksOutput { + risks := make([]*Risk, 0, len(riskPage.Data)) + for _, v := range riskPage.Data { + risks = append(risks, NewRisk(v)) + } + + var nextCursor *page.CursorKey + if len(riskPage.Data) > 0 { + cursorKey := riskPage.Data[len(riskPage.Data)-1].CursorKey(riskPage.Cursor.OrderBy.Field) + nextCursor = &cursorKey + } + + return ListRisksOutput{ + NextCursor: nextCursor, + Risks: risks, + } +} diff --git a/pkg/server/api/mcp/v1/types/types.go b/pkg/server/api/mcp/v1/types/types.go index 5196448f8..63f12128f 100644 --- a/pkg/server/api/mcp/v1/types/types.go +++ b/pkg/server/api/mcp/v1/types/types.go @@ -14,16 +14,24 @@ import ( var ( AddPeopleToolInputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["organization_id","full_name","primary_email_address","kind"],"properties":{"additional_email_addresses":{"type":"array","description":"Additional email addresses","items":{"type":"string"}},"contract_end_date":{"type":"string","description":"Contract end date","format":"date-time"},"contract_start_date":{"type":"string","description":"Contract start date","format":"date-time"},"full_name":{"type":"string","description":"Full name"},"kind":{"type":"string","enum":["EMPLOYEE","CONTRACTOR","SERVICE_ACCOUNT"]},"organization_id":{"type":"string","format":"string"},"position":{"type":"string","description":"Position"},"primary_email_address":{"type":"string","description":"Primary email address"}}}`) AddPeopleToolOutputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["people"],"properties":{"people":{"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"}}}}}`) + AddRiskToolInputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["organization_id","name","category","treatment","inherent_likelihood","inherent_impact"],"properties":{"category":{"type":"string","description":"Risk category"},"description":{"type":"string","description":"Risk description"},"inherent_impact":{"type":"integer","description":"Inherent impact"},"inherent_likelihood":{"type":"integer","description":"Inherent likelihood"},"name":{"type":"string","description":"Risk name"},"note":{"type":"string","description":"Risk note"},"organization_id":{"type":"string","format":"string"},"owner_id":{"type":"string","format":"string"},"residual_impact":{"type":"integer","description":"Residual impact"},"residual_likelihood":{"type":"integer","description":"Residual likelihood"},"treatment":{"type":"string","enum":["MITIGATED","ACCEPTED","AVOIDED","TRANSFERRED"]}}}`) + AddRiskToolOutputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["risk"],"properties":{"risk":{"type":"object","required":["id","organization_id","name","category","treatment","inherent_likelihood","inherent_impact","inherent_risk_score","residual_likelihood","residual_impact","residual_risk_score","note","created_at","updated_at"],"properties":{"category":{"type":"string","description":"Risk category"},"created_at":{"type":"string","description":"Creation timestamp","format":"date-time"},"description":{"description":"Risk description"},"id":{"type":"string","format":"string"},"inherent_impact":{"type":"integer","description":"Inherent impact"},"inherent_likelihood":{"type":"integer","description":"Inherent likelihood"},"inherent_risk_score":{"type":"integer","description":"Inherent risk score"},"name":{"type":"string","description":"Risk name"},"note":{"type":"string","description":"Risk note"},"organization_id":{"type":"string","format":"string"},"owner_id":{"description":"Owner ID"},"residual_impact":{"type":"integer","description":"Residual impact"},"residual_likelihood":{"type":"integer","description":"Residual likelihood"},"residual_risk_score":{"type":"integer","description":"Residual risk score"},"snapshot_id":{"description":"Snapshot ID"},"treatment":{"type":"string","enum":["MITIGATED","ACCEPTED","AVOIDED","TRANSFERRED"]},"updated_at":{"type":"string","description":"Update timestamp","format":"date-time"}}}}}`) AddVendorToolInputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["organization_id","name"],"properties":{"created_at":{"type":"string","description":"Creation timestamp","format":"date-time"},"description":{"type":"string","description":"Vendor description"},"name":{"type":"string","description":"Vendor name"},"organization_id":{"type":"string","format":"string"},"updated_at":{"type":"string","description":"Update timestamp","format":"date-time"}}}`) 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"}}}}}`) GetPeopleToolInputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["id"],"properties":{"id":{"type":"string","format":"string"}}}`) GetPeopleToolOutputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["people"],"properties":{"people":{"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"}}}}}`) + GetRiskToolInputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["id"],"properties":{"id":{"type":"string","format":"string"}}}`) + GetRiskToolOutputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["risk"],"properties":{"risk":{"type":"object","required":["id","organization_id","name","category","treatment","inherent_likelihood","inherent_impact","inherent_risk_score","residual_likelihood","residual_impact","residual_risk_score","note","created_at","updated_at"],"properties":{"category":{"type":"string","description":"Risk category"},"created_at":{"type":"string","description":"Creation timestamp","format":"date-time"},"description":{"description":"Risk description"},"id":{"type":"string","format":"string"},"inherent_impact":{"type":"integer","description":"Inherent impact"},"inherent_likelihood":{"type":"integer","description":"Inherent likelihood"},"inherent_risk_score":{"type":"integer","description":"Inherent risk score"},"name":{"type":"string","description":"Risk name"},"note":{"type":"string","description":"Risk note"},"organization_id":{"type":"string","format":"string"},"owner_id":{"description":"Owner ID"},"residual_impact":{"type":"integer","description":"Residual impact"},"residual_likelihood":{"type":"integer","description":"Residual likelihood"},"residual_risk_score":{"type":"integer","description":"Residual risk score"},"snapshot_id":{"description":"Snapshot ID"},"treatment":{"type":"string","enum":["MITIGATED","ACCEPTED","AVOIDED","TRANSFERRED"]},"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"}}}}}}`) 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"}}}}}}`) + ListRisksToolInputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["organization_id"],"properties":{"cursor":{"type":"string","format":"string"},"filter":{"type":"object","properties":{"query":{"type":"string","description":"Search query"},"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","CATEGORY","TREATMENT","INHERENT_RISK_SCORE","RESIDUAL_RISK_SCORE","OWNER_FULL_NAME"]}}},"organization_id":{"type":"string","format":"string"},"size":{"type":"integer","description":"Page size"}}}`) + ListRisksToolOutputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["risks"],"properties":{"next_cursor":{"type":"string","format":"string"},"risks":{"type":"array","items":{"type":"object","required":["id","organization_id","name","category","treatment","inherent_likelihood","inherent_impact","inherent_risk_score","residual_likelihood","residual_impact","residual_risk_score","note","created_at","updated_at"],"properties":{"category":{"type":"string","description":"Risk category"},"created_at":{"type":"string","description":"Creation timestamp","format":"date-time"},"description":{"description":"Risk description"},"id":{"type":"string","format":"string"},"inherent_impact":{"type":"integer","description":"Inherent impact"},"inherent_likelihood":{"type":"integer","description":"Inherent likelihood"},"inherent_risk_score":{"type":"integer","description":"Inherent risk score"},"name":{"type":"string","description":"Risk name"},"note":{"type":"string","description":"Risk note"},"organization_id":{"type":"string","format":"string"},"owner_id":{"description":"Owner ID"},"residual_impact":{"type":"integer","description":"Residual impact"},"residual_likelihood":{"type":"integer","description":"Residual likelihood"},"residual_risk_score":{"type":"integer","description":"Residual risk score"},"snapshot_id":{"description":"Snapshot ID"},"treatment":{"type":"string","enum":["MITIGATED","ACCEPTED","AVOIDED","TRANSFERRED"]},"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"}}}}}}`) + UpdateRiskToolInputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["id"],"properties":{"category":{"type":"string","description":"Risk category"},"description":{"description":"Risk description"},"id":{"type":"string","format":"string"},"inherent_impact":{"type":"integer","description":"Inherent impact"},"inherent_likelihood":{"type":"integer","description":"Inherent likelihood"},"name":{"type":"string","description":"Risk name"},"note":{"type":"string","description":"Risk note"},"owner_id":{"description":"Owner ID","anyOf":[{"type":"string","format":"string"},{"type":"null"}]},"residual_impact":{"type":"integer","description":"Residual impact"},"residual_likelihood":{"type":"integer","description":"Residual likelihood"},"treatment":{"type":"string","enum":["MITIGATED","ACCEPTED","AVOIDED","TRANSFERRED"]}}}`) + UpdateRiskToolOutputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["risk"],"properties":{"risk":{"type":"object","required":["id","organization_id","name","category","treatment","inherent_likelihood","inherent_impact","inherent_risk_score","residual_likelihood","residual_impact","residual_risk_score","note","created_at","updated_at"],"properties":{"category":{"type":"string","description":"Risk category"},"created_at":{"type":"string","description":"Creation timestamp","format":"date-time"},"description":{"description":"Risk description"},"id":{"type":"string","format":"string"},"inherent_impact":{"type":"integer","description":"Inherent impact"},"inherent_likelihood":{"type":"integer","description":"Inherent likelihood"},"inherent_risk_score":{"type":"integer","description":"Inherent risk score"},"name":{"type":"string","description":"Risk name"},"note":{"type":"string","description":"Risk note"},"organization_id":{"type":"string","format":"string"},"owner_id":{"description":"Owner ID"},"residual_impact":{"type":"integer","description":"Residual impact"},"residual_likelihood":{"type":"integer","description":"Residual likelihood"},"residual_risk_score":{"type":"integer","description":"Residual risk score"},"snapshot_id":{"description":"Snapshot ID"},"treatment":{"type":"string","enum":["MITIGATED","ACCEPTED","AVOIDED","TRANSFERRED"]},"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"}}}}}`) ) @@ -53,6 +61,37 @@ type AddPeopleOutput struct { People *People `json:"people"` } +// AddRiskInput represents the schema +type AddRiskInput struct { + // Risk category + Category string `json:"category"` + // Risk description + Description *string `json:"description,omitempty"` + // Inherent impact + InherentImpact int `json:"inherent_impact"` + // Inherent likelihood + InherentLikelihood int `json:"inherent_likelihood"` + // Risk name + Name string `json:"name"` + // Risk note + Note *string `json:"note,omitempty"` + // Organization ID + OrganizationID gid.GID `json:"organization_id"` + // Owner ID + OwnerID *gid.GID `json:"owner_id,omitempty"` + // Residual impact + ResidualImpact *int `json:"residual_impact,omitempty"` + // Residual likelihood + ResidualLikelihood *int `json:"residual_likelihood,omitempty"` + // Risk treatment + Treatment coredata.RiskTreatment `json:"treatment"` +} + +// AddRiskOutput represents the schema +type AddRiskOutput struct { + Risk *Risk `json:"risk"` +} + // AddVendorInput represents the schema type AddVendorInput struct { // Creation timestamp @@ -83,6 +122,17 @@ type GetPeopleOutput struct { People *People `json:"people"` } +// GetRiskInput represents the schema +type GetRiskInput struct { + // Risk ID + ID gid.GID `json:"id"` +} + +// GetRiskOutput represents the schema +type GetRiskOutput struct { + Risk *Risk `json:"risk"` +} + // ListOrganizationsInput represents the schema type ListOrganizationsInput struct { // Organization ID @@ -104,7 +154,7 @@ type ListPeopleInput struct { // Organization ID OrganizationID gid.GID `json:"organization_id"` // Page size - Size *int64 `json:"size,omitempty"` + Size *int `json:"size,omitempty"` } // ListPeopleOutput represents the schema @@ -114,6 +164,26 @@ type ListPeopleOutput struct { People []*People `json:"people"` } +// ListRisksInput represents the schema +type ListRisksInput struct { + // Page cursor + Cursor *page.CursorKey `json:"cursor,omitempty"` + Filter *ListRisksInputFilter `json:"filter,omitempty"` + // Risk order by + OrderBy *RiskOrderBy `json:"order_by,omitempty"` + // Organization ID + OrganizationID gid.GID `json:"organization_id"` + // Page size + Size *int `json:"size,omitempty"` +} + +// ListRisksOutput represents the schema +type ListRisksOutput struct { + // Next cursor + NextCursor *page.CursorKey `json:"next_cursor,omitempty"` + Risks []*Risk `json:"risks"` +} + // ListVendorsInput represents the schema type ListVendorsInput struct { // Page cursor @@ -124,7 +194,7 @@ type ListVendorsInput struct { // Organization ID OrganizationID gid.GID `json:"organization_id"` // Page size - Size *int64 `json:"size,omitempty"` + Size *int `json:"size,omitempty"` } // ListVendorsOutput represents the schema @@ -182,6 +252,83 @@ type PeopleOrderBy struct { Field coredata.PeopleOrderField `json:"field"` } +// Risk represents the schema +type Risk struct { + // Risk category + Category string `json:"category"` + // Creation timestamp + CreatedAt time.Time `json:"created_at"` + // Risk description + Description *string `json:"description,omitempty"` + // Risk ID + ID gid.GID `json:"id"` + // Inherent impact + InherentImpact int `json:"inherent_impact"` + // Inherent likelihood + InherentLikelihood int `json:"inherent_likelihood"` + // Inherent risk score + InherentRiskScore int `json:"inherent_risk_score"` + // Risk name + Name string `json:"name"` + // Risk note + Note string `json:"note"` + // Organization ID + OrganizationID gid.GID `json:"organization_id"` + // Owner ID + OwnerID *string `json:"owner_id,omitempty"` + // Residual impact + ResidualImpact int `json:"residual_impact"` + // Residual likelihood + ResidualLikelihood int `json:"residual_likelihood"` + // Residual risk score + ResidualRiskScore int `json:"residual_risk_score"` + // Snapshot ID + SnapshotID *string `json:"snapshot_id,omitempty"` + // Risk treatment + Treatment coredata.RiskTreatment `json:"treatment"` + // Update timestamp + UpdatedAt time.Time `json:"updated_at"` +} + +// RiskOrderBy represents the schema +type RiskOrderBy struct { + // Risk order direction + Direction page.OrderDirection `json:"direction"` + // Risk order field + Field coredata.RiskOrderField `json:"field"` +} + +// UpdateRiskInput represents the schema +type UpdateRiskInput struct { + // Risk category + Category *string `json:"category,omitempty"` + // Risk description + Description mcp.Omittable[*string] `json:"description,omitempty"` + // Risk ID + ID gid.GID `json:"id"` + // Inherent impact + InherentImpact *int `json:"inherent_impact,omitempty"` + // Inherent likelihood + InherentLikelihood *int `json:"inherent_likelihood,omitempty"` + // Risk name + Name *string `json:"name,omitempty"` + // Risk note + Note *string `json:"note,omitempty"` + // Owner ID + OwnerID mcp.Omittable[*gid.GID] `json:"owner_id,omitempty"` + // Residual impact + ResidualImpact *int `json:"residual_impact,omitempty"` + // Residual likelihood + ResidualLikelihood *int `json:"residual_likelihood,omitempty"` + // Risk treatment + Treatment *coredata.RiskTreatment `json:"treatment,omitempty"` +} + +// UpdateRiskOutput represents the schema +type UpdateRiskOutput struct { + Risk *Risk `json:"risk"` +} + // UpdateVendorInput represents the schema type UpdateVendorInput struct { // Vendor description @@ -227,6 +374,14 @@ type ListPeopleInputFilter struct { ExcludeContractEnded *bool `json:"exclude_contract_ended,omitempty"` } +// ListRisksInputFilter represents the schema +type ListRisksInputFilter struct { + // Search query + Query *string `json:"query,omitempty"` + // Snapshot ID + SnapshotID *gid.GID `json:"snapshot_id,omitempty"` +} + // ListVendorsInputFilter represents the schema type ListVendorsInputFilter struct { // Snapshot ID diff --git a/pkg/server/api/mcp/v1/v1_handler.go b/pkg/server/api/mcp/v1/v1_handler.go index 811215b8a..a6f2ee2c8 100644 --- a/pkg/server/api/mcp/v1/v1_handler.go +++ b/pkg/server/api/mcp/v1/v1_handler.go @@ -7,6 +7,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/modelcontextprotocol/go-sdk/mcp" "go.gearno.de/kit/log" + mcpgenmcp "go.probo.inc/mcpgen/mcp" "go.probo.inc/probo/pkg/auth" "go.probo.inc/probo/pkg/authz" "go.probo.inc/probo/pkg/gid" @@ -65,3 +66,11 @@ func NewMux(logger *log.Logger, proboSvc *probo.Service, authSvc *auth.Service, return r } + +func UnwrapOmittable[T any](field mcpgenmcp.Omittable[T]) *T { + if !field.IsSet() { + return nil + } + value, _ := field.Value() + return &value +}