From a13dbe1e78d27b296c8a7ca1ea2dbf5516aa5436 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 30 Jul 2026 08:53:08 +0000 Subject: [PATCH] Set full MCP tool annotation hints in schema Fill in readonly, destructive, idempotent, and openWorld on every tool so clients can rely on explicit values instead of MCP defaults (destructive and openWorld both default to true when omitted). Drop the temporary mcpgen vendor from this change; that lands in a separate pull request. Signed-off-by: Cursor Agent Co-authored-by: Bryan FRIMIN --- contrib/claude/mcp.md | 32 +- go.mod | 2 - go.sum | 2 + pkg/server/api/mcp/v1/specification.yaml | 593 +++++++ third_party/mcpgen/LICENSE | 20 - third_party/mcpgen/README.md | 381 ----- third_party/mcpgen/THIRD_PARTY.md | 12 - third_party/mcpgen/go.mod | 21 - third_party/mcpgen/go.sum | 32 - .../mcpgen/internal/codegen/generator.go | 1292 --------------- .../mcpgen/internal/codegen/generator_test.go | 1387 ----------------- .../internal/codegen/integration_test.go | 282 ---- third_party/mcpgen/internal/codegen/parser.go | 191 --- .../mcpgen/internal/codegen/parser_test.go | 484 ------ .../internal/codegen/templates/resolver.gotpl | 58 - .../codegen/templates/resolver_struct.gotpl | 22 - .../internal/codegen/templates/server.gotpl | 175 --- .../codegen/testdata/all_primitives.yaml | 222 --- .../testdata/config_based_types.golden | 31 - .../codegen/testdata/config_based_types.yaml | 45 - .../codegen/testdata/custom_types.yaml | 185 --- third_party/mcpgen/internal/codegen/types.go | 652 -------- .../mcpgen/internal/codegen/types_test.go | 1048 ------------- third_party/mcpgen/internal/config/config.go | 208 --- third_party/mcpgen/internal/config/spec.go | 114 -- third_party/mcpgen/internal/schema/schema.go | 85 - third_party/mcpgen/main.go | 185 --- third_party/mcpgen/mcp/omittable.go | 117 -- third_party/mcpgen/mcp/omittable_test.go | 201 --- third_party/mcpgen/mcp/recover.go | 63 - third_party/mcpgen/mcp/recover_test.go | 51 - third_party/mcpgen/mcp/schema.go | 77 - 32 files changed, 622 insertions(+), 7648 deletions(-) delete mode 100644 third_party/mcpgen/LICENSE delete mode 100644 third_party/mcpgen/README.md delete mode 100644 third_party/mcpgen/THIRD_PARTY.md delete mode 100644 third_party/mcpgen/go.mod delete mode 100644 third_party/mcpgen/go.sum delete mode 100644 third_party/mcpgen/internal/codegen/generator.go delete mode 100644 third_party/mcpgen/internal/codegen/generator_test.go delete mode 100644 third_party/mcpgen/internal/codegen/integration_test.go delete mode 100644 third_party/mcpgen/internal/codegen/parser.go delete mode 100644 third_party/mcpgen/internal/codegen/parser_test.go delete mode 100644 third_party/mcpgen/internal/codegen/templates/resolver.gotpl delete mode 100644 third_party/mcpgen/internal/codegen/templates/resolver_struct.gotpl delete mode 100644 third_party/mcpgen/internal/codegen/templates/server.gotpl delete mode 100644 third_party/mcpgen/internal/codegen/testdata/all_primitives.yaml delete mode 100644 third_party/mcpgen/internal/codegen/testdata/config_based_types.golden delete mode 100644 third_party/mcpgen/internal/codegen/testdata/config_based_types.yaml delete mode 100644 third_party/mcpgen/internal/codegen/testdata/custom_types.yaml delete mode 100644 third_party/mcpgen/internal/codegen/types.go delete mode 100644 third_party/mcpgen/internal/codegen/types_test.go delete mode 100644 third_party/mcpgen/internal/config/config.go delete mode 100644 third_party/mcpgen/internal/config/spec.go delete mode 100644 third_party/mcpgen/internal/schema/schema.go delete mode 100644 third_party/mcpgen/main.go delete mode 100644 third_party/mcpgen/mcp/omittable.go delete mode 100644 third_party/mcpgen/mcp/omittable_test.go delete mode 100644 third_party/mcpgen/mcp/recover.go delete mode 100644 third_party/mcpgen/mcp/recover_test.go delete mode 100644 third_party/mcpgen/mcp/schema.go diff --git a/contrib/claude/mcp.md b/contrib/claude/mcp.md index dbb848f32..41b796088 100644 --- a/contrib/claude/mcp.md +++ b/contrib/claude/mcp.md @@ -30,27 +30,49 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListThirdPartiesInput" outputSchema: $ref: "#/components/schemas/ListThirdPartiesOutput" + - name: addThirdParty + title: Add Third Party + description: Add a new thirdParty to the organization + hints: + readonly: false + destructive: false + idempotent: false + openWorld: false + inputSchema: + $ref: "#/components/schemas/AddThirdPartyInput" + outputSchema: + $ref: "#/components/schemas/AddThirdPartyOutput" - name: deleteThirdParty title: Delete Third Party description: Delete a thirdParty hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteThirdPartyInput" outputSchema: $ref: "#/components/schemas/DeleteThirdPartyOutput" ``` -`title` is the human-readable display name (emitted as MCP `title` / -`annotations.title`). `hints.readonly` and `hints.destructive` map to -`readOnlyHint` and `destructiveHint` so clients can distinguish reads, writes, -and deletes. Every delete/remove/unlink/cancel/void tool must set -`destructive: true`. +`title` is the human-readable display name (MCP `title` / `annotations.title`). + +`hints` map to MCP tool annotations. Set all of them explicitly — several +defaults are surprising (`destructiveHint` defaults to **true**, +`openWorldHint` defaults to **true**): + +| Hint | Reads | Additive writes | Deletes / regenerates | +|---|---|---|---| +| `readonly` | `true` | `false` | `false` | +| `destructive` | omit | `false` | `true` | +| `idempotent` | `true` | `false` for creates, `true` for updates | `true` | +| `openWorld` | `false` | `false` (unless the tool calls out, e.g. `vetThirdParty`) | `false` | Input/output schemas reference `components/schemas`. Map custom Go types with the `go.probo.inc/mcpgen/type` extension: diff --git a/go.mod b/go.mod index bb8d8b6b7..c732c47a4 100644 --- a/go.mod +++ b/go.mod @@ -272,5 +272,3 @@ tool ( ) replace github.com/elimity-com/scim => github.com/getprobo/scim v0.0.0-20260309220528-a952b258e8d3 - -replace go.probo.inc/mcpgen => ./third_party/mcpgen diff --git a/go.sum b/go.sum index 5621765a5..0c8968b23 100644 --- a/go.sum +++ b/go.sum @@ -651,6 +651,8 @@ go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/ go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.probo.inc/mcpgen v0.0.0-20260428172408-1496ba9b4619 h1:LHOdoF7kYRXFtSP97eWpF1dIf0dBLrunRLOeU/pXt9c= +go.probo.inc/mcpgen v0.0.0-20260428172408-1496ba9b4619/go.mod h1:HunWQGqLdMocExJh4tWaX7p+uRZ9GlKvBvOXHaFW6vM= go.step.sm/crypto v0.77.7 h1:6azC+pD678Vjju8yXnMDHCZJ+HzFaEmL3sCryiezTIA= go.step.sm/crypto v0.77.7/go.mod h1:OW/2sEHwTtDKq70PvSQ5B0JGy/CrLyDKOiVy3YvZMTQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= diff --git a/pkg/server/api/mcp/v1/specification.yaml b/pkg/server/api/mcp/v1/specification.yaml index eb886486a..64c709e36 100644 --- a/pkg/server/api/mcp/v1/specification.yaml +++ b/pkg/server/api/mcp/v1/specification.yaml @@ -12598,6 +12598,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListOrganizationsInput" outputSchema: @@ -12608,6 +12609,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListThirdPartiesInput" outputSchema: @@ -12618,6 +12620,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListChildThirdPartiesInput" outputSchema: @@ -12628,6 +12631,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListUsersInput" outputSchema: @@ -12638,6 +12642,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetUserInput" outputSchema: @@ -12647,6 +12652,9 @@ tools: description: Create a new user in the organization hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/CreateUserInput" outputSchema: @@ -12656,6 +12664,9 @@ tools: description: Invite a user (profile) to the organization hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/InviteUserInput" outputSchema: @@ -12665,6 +12676,9 @@ tools: description: Update an existing user (profile) hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateUserInput" outputSchema: @@ -12674,6 +12688,9 @@ tools: description: Update a membership role hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateMembershipInput" outputSchema: @@ -12684,6 +12701,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/RemoveUserInput" outputSchema: @@ -12698,6 +12717,9 @@ tools: >>>>>>> b29c4a15c (Annotate MCP tools with titles and hints) hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeactivateUserInput" outputSchema: @@ -12707,6 +12729,9 @@ tools: description: Add a new thirdParty to the organization hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddThirdPartyInput" outputSchema: @@ -12716,6 +12741,9 @@ tools: description: Update an existing thirdParty hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateThirdPartyInput" outputSchema: @@ -12726,6 +12754,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListThirdPartyRiskAssessmentsInput" outputSchema: @@ -12735,6 +12764,9 @@ tools: description: Add a new risk assessment for a thirdParty hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddThirdPartyRiskAssessmentInput" outputSchema: @@ -12745,6 +12777,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteThirdPartyInput" outputSchema: @@ -12755,6 +12789,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListThirdPartyContactsInput" outputSchema: @@ -12764,6 +12799,9 @@ tools: description: Add a new contact to a thirdParty hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddThirdPartyContactInput" outputSchema: @@ -12773,6 +12811,9 @@ tools: description: Update an existing thirdParty contact hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateThirdPartyContactInput" outputSchema: @@ -12783,6 +12824,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteThirdPartyContactInput" outputSchema: @@ -12793,6 +12836,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListThirdPartyServicesInput" outputSchema: @@ -12802,6 +12846,9 @@ tools: description: Add a new service to a thirdParty hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddThirdPartyServiceInput" outputSchema: @@ -12811,6 +12858,9 @@ tools: description: Update an existing thirdParty service hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateThirdPartyServiceInput" outputSchema: @@ -12821,6 +12871,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteThirdPartyServiceInput" outputSchema: @@ -12830,6 +12882,9 @@ tools: description: Start AI-powered vetting of a third party by crawling its website. Returns immediately; vetting runs in the background. hints: readonly: false + destructive: false + idempotent: false + openWorld: true inputSchema: $ref: "#/components/schemas/VetThirdPartyInput" outputSchema: @@ -12840,6 +12895,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListRisksInput" outputSchema: @@ -12850,6 +12906,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetRiskInput" outputSchema: @@ -12859,6 +12916,9 @@ tools: description: Add a new risk to the organization hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddRiskInput" outputSchema: @@ -12868,6 +12928,9 @@ tools: description: Update an existing risk hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateRiskInput" outputSchema: @@ -12878,6 +12941,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteRiskInput" outputSchema: @@ -12888,6 +12953,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListMeasuresInput" outputSchema: @@ -12898,6 +12964,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetMeasureInput" outputSchema: @@ -12907,6 +12974,9 @@ tools: description: Add a new measure to the organization hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddMeasureInput" outputSchema: @@ -12916,6 +12986,9 @@ tools: description: Update an existing measure hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateMeasureInput" outputSchema: @@ -12926,6 +12999,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteMeasureInput" outputSchema: @@ -12936,6 +13011,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListMeasureRisksInput" outputSchema: @@ -12946,6 +13022,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListMeasureControlsInput" outputSchema: @@ -12956,6 +13033,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListMeasureTasksInput" outputSchema: @@ -12966,6 +13044,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListMeasureEvidencesInput" outputSchema: @@ -12975,6 +13054,9 @@ tools: description: Link a measure to a resource (control, risk, document, or third party). The resource type is determined from the resource_id GID. hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/LinkMeasureInput" outputSchema: @@ -12985,6 +13067,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UnlinkMeasureInput" outputSchema: @@ -12995,6 +13079,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListMeasureDocumentsInput" outputSchema: @@ -13005,6 +13090,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListFrameworksInput" outputSchema: @@ -13015,6 +13101,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetFrameworkInput" outputSchema: @@ -13024,6 +13111,9 @@ tools: description: Add a new framework to the organization hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddFrameworkInput" outputSchema: @@ -13033,6 +13123,9 @@ tools: description: Update an existing framework hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateFrameworkInput" outputSchema: @@ -13043,6 +13136,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListAssetsInput" outputSchema: @@ -13053,6 +13147,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetAssetInput" outputSchema: @@ -13062,6 +13157,9 @@ tools: description: Add a new asset to the organization hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddAssetInput" outputSchema: @@ -13071,6 +13169,9 @@ tools: description: Update an existing asset hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateAssetInput" outputSchema: @@ -13081,6 +13182,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteAssetInput" outputSchema: @@ -13091,6 +13194,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListDataInput" outputSchema: @@ -13101,6 +13205,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetDatumInput" outputSchema: @@ -13110,6 +13215,9 @@ tools: description: Add a new datum to the organization hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddDatumInput" outputSchema: @@ -13119,6 +13227,9 @@ tools: description: Update an existing datum hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateDatumInput" outputSchema: @@ -13129,6 +13240,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteDatumInput" outputSchema: @@ -13139,6 +13252,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListFindingsInput" outputSchema: @@ -13149,6 +13263,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetFindingInput" outputSchema: @@ -13158,6 +13273,9 @@ tools: description: Add a new finding (nonconformity, observation, or exception) to the organization hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddFindingInput" outputSchema: @@ -13167,6 +13285,9 @@ tools: description: Update an existing finding hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateFindingInput" outputSchema: @@ -13177,6 +13298,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteFindingInput" outputSchema: @@ -13186,6 +13309,9 @@ tools: description: Link a finding to an audit with a reference ID hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/LinkFindingAuditInput" outputSchema: @@ -13196,6 +13322,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UnlinkFindingAuditInput" outputSchema: @@ -13206,6 +13334,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListFindingAuditsInput" outputSchema: @@ -13216,6 +13345,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListObligationsInput" outputSchema: @@ -13226,6 +13356,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetObligationInput" outputSchema: @@ -13235,6 +13366,9 @@ tools: description: Add a new obligation to the organization hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddObligationInput" outputSchema: @@ -13244,6 +13378,9 @@ tools: description: Update an existing obligation hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateObligationInput" outputSchema: @@ -13254,6 +13391,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteObligationInput" outputSchema: @@ -13264,6 +13403,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListProcessingActivitiesInput" outputSchema: @@ -13274,6 +13414,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetProcessingActivityInput" outputSchema: @@ -13283,6 +13424,9 @@ tools: description: Add a new processing activity to the organization hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddProcessingActivityInput" outputSchema: @@ -13292,6 +13436,9 @@ tools: description: Update an existing processing activity hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateProcessingActivityInput" outputSchema: @@ -13302,6 +13449,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteProcessingActivityInput" outputSchema: @@ -13312,6 +13461,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListDataProtectionImpactAssessmentsInput" outputSchema: @@ -13322,6 +13472,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetDataProtectionImpactAssessmentInput" outputSchema: @@ -13331,6 +13482,9 @@ tools: description: Add a new data protection impact assessment (DPIA) for a processing activity hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddDataProtectionImpactAssessmentInput" outputSchema: @@ -13340,6 +13494,9 @@ tools: description: Update an existing data protection impact assessment (DPIA) hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateDataProtectionImpactAssessmentInput" outputSchema: @@ -13350,6 +13507,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteDataProtectionImpactAssessmentInput" outputSchema: @@ -13360,6 +13519,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListTransferImpactAssessmentsInput" outputSchema: @@ -13370,6 +13530,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetTransferImpactAssessmentInput" outputSchema: @@ -13379,6 +13540,9 @@ tools: description: Add a new transfer impact assessment (TIA) for a processing activity hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddTransferImpactAssessmentInput" outputSchema: @@ -13388,6 +13552,9 @@ tools: description: Update an existing transfer impact assessment (TIA) hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateTransferImpactAssessmentInput" outputSchema: @@ -13398,6 +13565,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteTransferImpactAssessmentInput" outputSchema: @@ -13408,6 +13577,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListAuditsInput" outputSchema: @@ -13418,6 +13588,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetAuditInput" outputSchema: @@ -13427,6 +13598,9 @@ tools: description: Add a new audit to the organization hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddAuditInput" outputSchema: @@ -13436,6 +13610,9 @@ tools: description: Update an existing audit hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateAuditInput" outputSchema: @@ -13446,6 +13623,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteAuditInput" outputSchema: @@ -13456,6 +13635,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetAuditReportUrlInput" outputSchema: @@ -13466,6 +13646,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListControlsInput" outputSchema: @@ -13476,6 +13657,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetControlInput" outputSchema: @@ -13485,6 +13667,9 @@ tools: description: Add a new control to a framework hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddControlInput" outputSchema: @@ -13494,6 +13679,9 @@ tools: description: Update an existing control hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateControlInput" outputSchema: @@ -13503,6 +13691,9 @@ tools: description: Link a resource to a control (measure, document, audit, or obligation). The resource type is determined from the resource_id GID. hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/LinkControlInput" outputSchema: @@ -13513,6 +13704,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UnlinkControlInput" outputSchema: @@ -13523,6 +13716,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListControlObligationsInput" outputSchema: @@ -13533,6 +13727,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListControlMeasuresInput" outputSchema: @@ -13543,6 +13738,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListControlDocumentsInput" outputSchema: @@ -13553,6 +13749,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListControlAuditsInput" outputSchema: @@ -13563,6 +13760,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListRiskObligationsInput" outputSchema: @@ -13572,6 +13770,9 @@ tools: description: Link a risk to a resource (document, measure, or obligation). The resource type is determined from the resource_id GID. hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/LinkRiskInput" outputSchema: @@ -13582,6 +13783,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UnlinkRiskInput" outputSchema: @@ -13592,6 +13795,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListTasksInput" outputSchema: @@ -13602,6 +13806,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetTaskInput" outputSchema: @@ -13611,6 +13816,9 @@ tools: description: Add a new task to the organization hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddTaskInput" outputSchema: @@ -13620,6 +13828,9 @@ tools: description: Update an existing task hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateTaskInput" outputSchema: @@ -13629,6 +13840,9 @@ tools: description: Assign a task to a person hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/AssignTaskInput" outputSchema: @@ -13638,6 +13852,9 @@ tools: description: Unassign a task from a person hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UnassignTaskInput" outputSchema: @@ -13648,6 +13865,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteTaskInput" outputSchema: @@ -13658,6 +13877,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListDocumentsInput" outputSchema: @@ -13668,6 +13888,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetDocumentInput" outputSchema: @@ -13677,6 +13898,9 @@ tools: description: Add a new document to the organization hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddDocumentInput" outputSchema: @@ -13686,6 +13910,9 @@ tools: description: Update an existing document hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateDocumentInput" outputSchema: @@ -13696,6 +13923,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteDocumentDraftInput" outputSchema: @@ -13705,6 +13934,9 @@ tools: description: Archive a document to prevent further modifications hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ArchiveDocumentInput" outputSchema: @@ -13714,6 +13946,9 @@ tools: description: Unarchive a document to allow modifications again hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UnarchiveDocumentInput" outputSchema: @@ -13724,6 +13959,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListDocumentVersionsInput" outputSchema: @@ -13734,6 +13970,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetDocumentVersionInput" outputSchema: @@ -13743,6 +13980,9 @@ tools: description: Publish the latest draft of a document. Set minor=true to publish a minor version (no approval flow). When minor=false, providing approver_ids triggers an approval request as a new major version; otherwise the draft is published immediately as a new major version. hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/PublishDocumentInput" outputSchema: @@ -13753,6 +13993,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteDocumentInput" outputSchema: @@ -13763,6 +14005,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListDocumentVersionSignaturesInput" outputSchema: @@ -13773,6 +14016,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetDocumentVersionSignatureInput" outputSchema: @@ -13782,6 +14026,9 @@ tools: description: Request a signature for a document version hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/RequestDocumentVersionSignatureInput" outputSchema: @@ -13792,6 +14039,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/CancelSignatureRequestInput" outputSchema: @@ -13802,6 +14051,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/VoidDocumentVersionApprovalInput" outputSchema: @@ -13812,6 +14063,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListStatementsOfApplicabilityInput" outputSchema: @@ -13822,6 +14074,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetStatementOfApplicabilityInput" outputSchema: @@ -13831,6 +14084,9 @@ tools: description: Add a new statement of applicability to the organization hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddStatementOfApplicabilityInput" outputSchema: @@ -13840,6 +14096,9 @@ tools: description: Update an existing statement of applicability hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateStatementOfApplicabilityInput" outputSchema: @@ -13850,6 +14109,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteStatementOfApplicabilityInput" outputSchema: @@ -13859,6 +14120,9 @@ tools: description: Publish the data list for an organization as a document. If a document already exists, a new version is created. hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/PublishDataListInput" outputSchema: @@ -13868,6 +14132,9 @@ tools: description: Publish the asset list for an organization as a document. If a document already exists, a new version is created. hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/PublishAssetListInput" outputSchema: @@ -13877,6 +14144,9 @@ tools: description: Publish the finding register for an organization as a document. If a document already exists, a new version is created. hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/PublishFindingListInput" outputSchema: @@ -13886,6 +14156,9 @@ tools: description: Publish the obligation register for an organization as a document. If a document already exists, a new version is created. hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/PublishObligationListInput" outputSchema: @@ -13895,6 +14168,9 @@ tools: description: Publish the processing activity register for an organization as a document. If a document already exists, a new version is created. hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/PublishProcessingActivityListInput" outputSchema: @@ -13904,6 +14180,9 @@ tools: description: Publish the Data Protection Impact Assessment register for an organization as a document. If a document already exists, a new version is created. hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/PublishDataProtectionImpactAssessmentListInput" outputSchema: @@ -13913,6 +14192,9 @@ tools: description: Publish the Transfer Impact Assessment register for an organization as a document. If a document already exists, a new version is created. hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/PublishTransferImpactAssessmentListInput" outputSchema: @@ -13922,6 +14204,9 @@ tools: description: Publish the thirdParty register for an organization as a document. If a document already exists, a new version is created. hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/PublishThirdPartyListInput" outputSchema: @@ -13931,6 +14216,9 @@ tools: description: Publish the risk register for an organization as a document. If a document already exists, a new version is created. hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/PublishRiskListInput" outputSchema: @@ -13940,6 +14228,9 @@ tools: description: Publish a statement of applicability as a document. If a document already exists, a new version is created. hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/PublishStatementOfApplicabilityInput" outputSchema: @@ -13950,6 +14241,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListApplicabilityStatementsInput" outputSchema: @@ -13960,6 +14252,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetApplicabilityStatementInput" outputSchema: @@ -13969,6 +14262,9 @@ tools: description: Add a control to a statement of applicability with an applicability decision hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddApplicabilityStatementInput" outputSchema: @@ -13978,6 +14274,9 @@ tools: description: Update the applicability and justification of an applicability statement hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateApplicabilityStatementInput" outputSchema: @@ -13988,6 +14287,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteApplicabilityStatementInput" outputSchema: @@ -13998,6 +14299,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListAccessReviewCampaignsInput" outputSchema: @@ -14008,6 +14310,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListAccessEntriesInput" outputSchema: @@ -14018,6 +14321,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetAccessReviewStatisticsInput" outputSchema: @@ -14027,6 +14331,9 @@ tools: description: Record a decision on an access entry (APPROVED, REVOKE, DEFER, or ESCALATE). Non-APPROVED decisions require a decision_note. hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/RecordAccessReviewEntryDecisionMCPInput" outputSchema: @@ -14036,6 +14343,9 @@ tools: description: Record decisions on multiple access entries in a single batch. Non-APPROVED decisions require a decision_note. hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/RecordAccessReviewEntryDecisionsMCPInput" outputSchema: @@ -14045,6 +14355,9 @@ tools: description: Flag an access entry with one or more flags during review (ORPHANED, INACTIVE, EXCESSIVE, ROLE_MISMATCH, NEW, etc.). Optionally provide reasons. hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/FlagAccessReviewEntryMCPInput" outputSchema: @@ -14054,6 +14367,9 @@ tools: description: Close an access review campaign. All entries must have been decided (no PENDING entries). hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/CloseAccessReviewCampaignMCPInput" outputSchema: @@ -14064,6 +14380,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListAccessReviewSourcesInput" outputSchema: @@ -14073,6 +14390,9 @@ tools: description: Create a new access source for an organization hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/CreateAccessReviewSourceMCPInput" outputSchema: @@ -14082,6 +14402,9 @@ tools: description: Update an existing access source hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateAccessReviewSourceMCPInput" outputSchema: @@ -14092,6 +14415,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteAccessReviewSourceMCPInput" outputSchema: @@ -14101,6 +14426,9 @@ tools: description: Create a new access review campaign for an organization hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/CreateAccessReviewCampaignMCPInput" outputSchema: @@ -14110,6 +14438,9 @@ tools: description: Update an existing access review campaign hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateAccessReviewCampaignMCPInput" outputSchema: @@ -14120,6 +14451,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteAccessReviewCampaignMCPInput" outputSchema: @@ -14129,6 +14462,9 @@ tools: description: Start an access review campaign. Triggers data fetching from all configured scope sources. hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/StartAccessReviewCampaignMCPInput" outputSchema: @@ -14139,6 +14475,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/CancelAccessReviewCampaignMCPInput" outputSchema: @@ -14148,6 +14486,9 @@ tools: description: Add an access source to an access review campaign's scope hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddAccessReviewCampaignSourceMCPInput" outputSchema: @@ -14158,6 +14499,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/RemoveAccessReviewCampaignSourceMCPInput" outputSchema: @@ -14168,6 +14511,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetOrganizationContextInput" outputSchema: @@ -14177,6 +14521,9 @@ tools: description: Update the organization context sections (product, architecture, team, processes, customers) hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateOrganizationContextInput" outputSchema: @@ -14187,6 +14534,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetAuditLogEntryInput" outputSchema: @@ -14197,6 +14545,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListAuditLogEntriesInput" outputSchema: @@ -14206,7 +14555,9 @@ tools: description: Request an export of audit log entries for the organization within a time range. The export will be emailed as a JSONL download link. hints: readonly: false + destructive: false idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/RequestAuditLogExportInput" outputSchema: @@ -14216,7 +14567,9 @@ tools: description: Request an export of SCIM events for the organization within a time range. The export will be emailed as a JSONL download link. hints: readonly: false + destructive: false idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/RequestSCIMEventExportInput" outputSchema: @@ -14227,6 +14580,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListWebhookSubscriptionsInput" outputSchema: @@ -14237,6 +14591,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetWebhookSubscriptionInput" outputSchema: @@ -14246,6 +14601,9 @@ tools: description: Create a new webhook subscription for the organization. The endpoint URL must use HTTPS. Selected events determine which events trigger webhook deliveries. hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/CreateWebhookSubscriptionInput" outputSchema: @@ -14255,6 +14613,9 @@ tools: description: Update a webhook subscription's endpoint URL or selected events hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateWebhookSubscriptionInput" outputSchema: @@ -14265,6 +14626,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteWebhookSubscriptionInput" outputSchema: @@ -14275,6 +14638,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListWebhookEventsInput" outputSchema: @@ -14285,6 +14649,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListDocumentVersionApprovalQuorumsInput" outputSchema: @@ -14295,6 +14660,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetDocumentVersionApprovalQuorumInput" outputSchema: @@ -14305,6 +14671,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListDocumentVersionApprovalDecisionsInput" outputSchema: @@ -14315,6 +14682,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetDocumentVersionApprovalDecisionInput" outputSchema: @@ -14325,6 +14693,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListRightsRequestsInput" outputSchema: @@ -14335,6 +14704,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetRightsRequestInput" outputSchema: @@ -14344,6 +14714,9 @@ tools: description: Add a new rights request to the organization hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddRightsRequestInput" outputSchema: @@ -14353,6 +14726,9 @@ tools: description: Update an existing rights request hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateRightsRequestInput" outputSchema: @@ -14363,6 +14739,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteRightsRequestInput" outputSchema: @@ -14373,6 +14751,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetCompliancePortalInput" outputSchema: @@ -14382,6 +14761,9 @@ tools: description: Update a compliance portal's settings hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateCompliancePortalInput" outputSchema: @@ -14392,6 +14774,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListCompliancePortalReferencesInput" outputSchema: @@ -14401,6 +14784,9 @@ tools: description: Add a new reference to a compliance portal hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddCompliancePortalReferenceInput" outputSchema: @@ -14410,6 +14796,9 @@ tools: description: Update an existing compliance portal reference hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateCompliancePortalReferenceInput" outputSchema: @@ -14420,6 +14809,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteCompliancePortalReferenceInput" outputSchema: @@ -14430,6 +14821,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListCommitmentGroupsInput" outputSchema: @@ -14439,6 +14831,9 @@ tools: description: Add a new commitment group to a trust center hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddCommitmentGroupInput" outputSchema: @@ -14448,6 +14843,9 @@ tools: description: Update an existing commitment group hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateCommitmentGroupInput" outputSchema: @@ -14458,6 +14856,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteCommitmentGroupInput" outputSchema: @@ -14468,6 +14868,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListCommitmentsInput" outputSchema: @@ -14477,6 +14878,9 @@ tools: description: Add a new commitment to a commitment group hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddCommitmentInput" outputSchema: @@ -14486,6 +14890,9 @@ tools: description: Update an existing commitment hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateCommitmentInput" outputSchema: @@ -14496,6 +14903,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteCommitmentInput" outputSchema: @@ -14505,6 +14914,9 @@ tools: description: Set a resource alias for a resource hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/SetResourceAliasInput" outputSchema: @@ -14515,6 +14927,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/RemoveResourceAliasInput" outputSchema: @@ -14525,6 +14939,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListCompliancePortalFilesInput" outputSchema: @@ -14535,6 +14950,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteCompliancePortalFileInput" outputSchema: @@ -14545,6 +14962,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListComplianceCustomLinksInput" outputSchema: @@ -14554,6 +14972,9 @@ tools: description: Add a new compliance custom link to a compliance portal hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddComplianceCustomLinkInput" outputSchema: @@ -14563,6 +14984,9 @@ tools: description: Update an existing compliance custom link hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateComplianceCustomLinkInput" outputSchema: @@ -14573,6 +14997,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteComplianceCustomLinkInput" outputSchema: @@ -14582,6 +15008,9 @@ tools: description: Create a custom domain for a compliance page hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/CreateCustomDomainInput" outputSchema: @@ -14592,6 +15021,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteCustomDomainInput" outputSchema: @@ -14602,6 +15033,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListCookieBannersInput" outputSchema: @@ -14612,6 +15044,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetCookieBannerInput" outputSchema: @@ -14621,6 +15054,9 @@ tools: description: Create a new cookie banner for an organization hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddCookieBannerInput" outputSchema: @@ -14630,6 +15066,9 @@ tools: description: Update an existing cookie banner hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateCookieBannerInput" outputSchema: @@ -14640,6 +15079,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteCookieBannerInput" outputSchema: @@ -14649,6 +15090,9 @@ tools: description: Activate a cookie banner hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ActivateCookieBannerInput" outputSchema: @@ -14658,6 +15102,9 @@ tools: description: Deactivate a cookie banner hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeactivateCookieBannerInput" outputSchema: @@ -14668,6 +15115,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListCookieCategoriesInput" outputSchema: @@ -14678,6 +15126,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetCookieCategoryInput" outputSchema: @@ -14687,6 +15136,9 @@ tools: description: Create a new cookie category for a banner hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddCookieCategoryInput" outputSchema: @@ -14696,6 +15148,9 @@ tools: description: Update an existing cookie category hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateCookieCategoryInput" outputSchema: @@ -14706,6 +15161,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteCookieCategoryInput" outputSchema: @@ -14715,6 +15172,9 @@ tools: description: Change the display order rank of a cookie category hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ReorderCookieCategoryInput" outputSchema: @@ -14725,6 +15185,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListTrackerPatternsInput" outputSchema: @@ -14735,6 +15196,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetTrackerPatternInput" outputSchema: @@ -14744,6 +15206,9 @@ tools: description: Create a new tracker pattern for a category hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddTrackerPatternInput" outputSchema: @@ -14753,6 +15218,9 @@ tools: description: Update an existing tracker pattern hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateTrackerPatternInput" outputSchema: @@ -14763,6 +15231,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteTrackerPatternInput" outputSchema: @@ -14772,6 +15242,9 @@ tools: description: Move a tracker pattern to a different category hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/MoveTrackerPatternToCategoryInput" outputSchema: @@ -14782,6 +15255,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListTrackerResourcesInput" outputSchema: @@ -14792,6 +15266,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetTrackerResourceInput" outputSchema: @@ -14801,6 +15276,9 @@ tools: description: Create a new tracker resource for a category hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddTrackerResourceInput" outputSchema: @@ -14810,6 +15288,9 @@ tools: description: Update an existing tracker resource hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateTrackerResourceInput" outputSchema: @@ -14820,6 +15301,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteTrackerResourceInput" outputSchema: @@ -14829,6 +15312,9 @@ tools: description: Move a tracker resource to a different category hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/MoveTrackerResourceToCategoryInput" outputSchema: @@ -14838,6 +15324,9 @@ tools: description: Publish the current draft version of a cookie banner hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/PublishCookieBannerVersionInput" outputSchema: @@ -14847,6 +15336,9 @@ tools: description: Re-arm tracker policy generation for a cookie banner that already has a published version. Returns immediately; the policy document is regenerated in the background. hints: readonly: false + destructive: true + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/RegenerateCookieBannerTrackerPolicyInput" outputSchema: @@ -14857,6 +15349,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListCookieBannerVersionsInput" outputSchema: @@ -14866,6 +15359,9 @@ tools: description: Insert or update a cookie banner translation for a language hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpsertCookieBannerTranslationInput" outputSchema: @@ -14876,6 +15372,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListCookieConsentRecordsInput" outputSchema: @@ -14886,6 +15383,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetCookieConsentRecordInput" outputSchema: @@ -14896,6 +15394,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetSCIMConfigurationInput" outputSchema: @@ -14905,6 +15404,9 @@ tools: description: Create a SCIM configuration for an organization. Optionally provide a connector ID to also create a SCIM bridge. hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/CreateSCIMConfigurationInput" outputSchema: @@ -14915,6 +15417,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteSCIMConfigurationInput" outputSchema: @@ -14924,6 +15428,9 @@ tools: description: Regenerate the bearer token for a SCIM configuration hints: readonly: false + destructive: true + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/RegenerateSCIMTokenInput" outputSchema: @@ -14934,6 +15441,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetSCIMBridgeInput" outputSchema: @@ -14943,6 +15451,9 @@ tools: description: Update a SCIM bridge's excluded user names hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateSCIMBridgeInput" outputSchema: @@ -14953,6 +15464,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListSCIMEventsInput" outputSchema: @@ -14963,6 +15475,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListRiskAssessmentsInput" outputSchema: @@ -14973,6 +15486,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetRiskAssessmentInput" outputSchema: @@ -14982,6 +15496,9 @@ tools: description: Create a new risk assessment hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddRiskAssessmentInput" outputSchema: @@ -14991,6 +15508,9 @@ tools: description: Update an existing risk assessment hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateRiskAssessmentInput" outputSchema: @@ -15001,6 +15521,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteRiskAssessmentInput" outputSchema: @@ -15011,6 +15533,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListRiskAssessmentScopesInput" outputSchema: @@ -15021,6 +15544,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetRiskAssessmentScopeInput" outputSchema: @@ -15031,6 +15555,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetRiskAssessmentScopeMermaidChartInput" outputSchema: @@ -15040,6 +15565,9 @@ tools: description: Create a new risk assessment scope hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddRiskAssessmentScopeInput" outputSchema: @@ -15049,6 +15577,9 @@ tools: description: Update an existing risk assessment scope hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateRiskAssessmentScopeInput" outputSchema: @@ -15059,6 +15590,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteRiskAssessmentScopeInput" outputSchema: @@ -15069,6 +15602,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListRiskAssessmentNodesInput" outputSchema: @@ -15079,6 +15613,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetRiskAssessmentNodeInput" outputSchema: @@ -15088,6 +15623,9 @@ tools: description: Create a new risk assessment node hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddRiskAssessmentNodeInput" outputSchema: @@ -15097,6 +15635,9 @@ tools: description: Update an existing risk assessment node hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateRiskAssessmentNodeInput" outputSchema: @@ -15107,6 +15648,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteRiskAssessmentNodeInput" outputSchema: @@ -15117,6 +15660,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListRiskAssessmentBoundariesInput" outputSchema: @@ -15127,6 +15671,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetRiskAssessmentBoundaryInput" outputSchema: @@ -15136,6 +15681,9 @@ tools: description: Create a new risk assessment boundary hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddRiskAssessmentBoundaryInput" outputSchema: @@ -15145,6 +15693,9 @@ tools: description: Update an existing risk assessment boundary hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateRiskAssessmentBoundaryInput" outputSchema: @@ -15155,6 +15706,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteRiskAssessmentBoundaryInput" outputSchema: @@ -15165,6 +15718,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListRiskAssessmentProcessesInput" outputSchema: @@ -15175,6 +15729,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetRiskAssessmentProcessInput" outputSchema: @@ -15184,6 +15739,9 @@ tools: description: Create a new risk assessment process hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddRiskAssessmentProcessInput" outputSchema: @@ -15193,6 +15751,9 @@ tools: description: Update an existing risk assessment process hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateRiskAssessmentProcessInput" outputSchema: @@ -15203,6 +15764,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteRiskAssessmentProcessInput" outputSchema: @@ -15213,6 +15776,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListRiskAssessmentThreatsInput" outputSchema: @@ -15223,6 +15787,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetRiskAssessmentThreatInput" outputSchema: @@ -15232,6 +15797,9 @@ tools: description: Create a new risk assessment threat hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddRiskAssessmentThreatInput" outputSchema: @@ -15241,6 +15809,9 @@ tools: description: Update an existing risk assessment threat hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateRiskAssessmentThreatInput" outputSchema: @@ -15251,6 +15822,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteRiskAssessmentThreatInput" outputSchema: @@ -15261,6 +15834,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/ListRiskAssessmentScenariosInput" outputSchema: @@ -15271,6 +15845,7 @@ tools: hints: readonly: true idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/GetRiskAssessmentScenarioInput" outputSchema: @@ -15280,6 +15855,9 @@ tools: description: Create a new risk assessment scenario hints: readonly: false + destructive: false + idempotent: false + openWorld: false inputSchema: $ref: "#/components/schemas/AddRiskAssessmentScenarioInput" outputSchema: @@ -15289,6 +15867,9 @@ tools: description: Update an existing risk assessment scenario hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UpdateRiskAssessmentScenarioInput" outputSchema: @@ -15299,6 +15880,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/DeleteRiskAssessmentScenarioInput" outputSchema: @@ -15308,6 +15891,9 @@ tools: description: Link a threat to a risk assessment scenario hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/LinkRiskAssessmentScenarioThreatInput" outputSchema: @@ -15318,6 +15904,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UnlinkRiskAssessmentScenarioThreatInput" outputSchema: @@ -15327,6 +15915,9 @@ tools: description: Link a risk to a risk assessment scenario hints: readonly: false + destructive: false + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/LinkRiskAssessmentScenarioRiskInput" outputSchema: @@ -15337,6 +15928,8 @@ tools: hints: readonly: false destructive: true + idempotent: true + openWorld: false inputSchema: $ref: "#/components/schemas/UnlinkRiskAssessmentScenarioRiskInput" outputSchema: diff --git a/third_party/mcpgen/LICENSE b/third_party/mcpgen/LICENSE deleted file mode 100644 index 4b33f7ee9..000000000 --- a/third_party/mcpgen/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -Copyright 2025 Probo Inc - - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files (the - ā€œSoftwareā€), to deal in the Software without restriction, including - without limitation the rights to use, copy, modify, merge, publish, - distribute, sublicense, and/or sell copies of the Software, and to - permit persons to whom the Software is furnished to do so, subject to - the following conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED ā€œAS ISā€, WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/third_party/mcpgen/README.md b/third_party/mcpgen/README.md deleted file mode 100644 index 3ad12c8f7..000000000 --- a/third_party/mcpgen/README.md +++ /dev/null @@ -1,381 +0,0 @@ -# mcpgen - -## Overview - -mcpgen is a code generator for Model Context Protocol (MCP) servers in Go, inspired by [gqlgen](https://github.com/99designs/gqlgen). - -mcpgen takes a schema-first approach to building MCP servers. Define your tools, resources, and prompts in a YAML configuration file with JSON Schema definitions, and mcpgen generates type-safe Go code including: - -- Type-safe Go structs from JSON Schemas -- MCP server boilerplate with the official [go-sdk](https://github.com/modelcontextprotocol/go-sdk) -- Handler function stubs ready for your business logic - -## Features - -- **Schema-First Development**: Define MCP primitives (tools, resources, prompts) in YAML with JSON Schema -- **Type-Safe Code Generation**: Generate Go structs from JSON Schema Draft 2020-12 -- **Custom Type Mapping**: Use your own Go types instead of generated ones (like gqlgen) -- **Omittable Fields**: Distinguish between "not set", "null", and "value" with `go.probo.inc/mcpgen/omittable` (like gqlgen's `@goField(omittable: true)`) -- **Official SDK Integration**: Uses the official `modelcontextprotocol/go-sdk` -- **Handler Preservation**: Regeneration preserves your handler implementations -- **gqlgen-Inspired**: Familiar workflow if you've used gqlgen - -## Installation - -```bash -go install go.probo.inc/mcpgen@latest -``` - -Or build from source: - -```bash -git clone https://github.com/probo-inc/mcpgen -cd mcpgen -go build -o mcpgen -``` - -## Quick Start - -### 1. Initialize a new project - -```bash -mcpgen init my-mcp-server -cd my-mcp-server -``` - -This creates: -``` -my-mcp-server/ -ā”œā”€ā”€ mcpgen.yaml # Configuration file -ā”œā”€ā”€ schemas/ # JSON Schema definitions -│ └── example_input.json -ā”œā”€ā”€ main.go # Entry point -└── README.md -``` - -### 2. Define your MCP primitives - -Edit `mcpgen.yaml`: - -```yaml -server: - name: my-mcp-server - version: 1.0.0 - -tools: - - name: calculate - description: Perform arithmetic operations - input_schema: schemas/calculate_input.json - -resources: - - uri: docs://readme - name: Project README - description: The project README file - mime_type: text/markdown - -prompts: - - name: greeting - description: A friendly greeting - arguments: - - name: name - description: Name of person to greet - required: false -``` - -### 3. Create JSON Schemas - -Define schemas in the `schemas/` directory: - -```json -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "properties": { - "operation": { - "type": "string", - "enum": ["add", "subtract", "multiply", "divide"] - }, - "a": { - "type": "number", - "description": "First operand" - }, - "b": { - "type": "number", - "description": "Second operand" - } - }, - "required": ["operation", "a", "b"] -} -``` - -### 4. Generate code - -```bash -mcpgen generate -``` - -This generates: -- `generated/models.go` - Type-safe Go structs -- `generated/server.go` - MCP server setup -- `generated/resolver.go` - Handler stubs (first time only) - -### 5. Implement handlers - -Edit `generated/resolver.go`: - -```go -func (r *Resolver) Calculate(ctx context.Context, req *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, map[string]any, error) { - operation := args["operation"].(string) - a := args["a"].(float64) - b := args["b"].(float64) - - var result float64 - switch operation { - case "add": - result = a + b - case "subtract": - result = a - b - case "multiply": - result = a * b - case "divide": - if b == 0 { - return nil, nil, fmt.Errorf("division by zero") - } - result = a / b - } - - return &mcp.CallToolResult{ - Content: []mcp.Content{ - &mcp.TextContent{ - Text: fmt.Sprintf("Result: %f", result), - }, - }, - }, map[string]any{"result": result}, nil -} -``` - -### 6. Build and run - -```bash -go mod init my-mcp-server -go mod tidy -go build -o server -./server -``` - -## Configuration Reference - -### Server Configuration - -```yaml -server: - name: my-server # Required: Server name - version: 1.0.0 # Required: Server version -``` - -### Code Generation Options - -```yaml -exec: - filename: generated/server.go # Server code output - package: generated # Package name - -model: - filename: generated/models.go # Models output - package: generated # Package name - -resolver: - filename: generated/resolver.go # Resolver stubs output - type: Resolver # Resolver type name - package: generated # Package name - preserve_resolver: true # Don't overwrite on regeneration -``` - -### Tools - -```yaml -tools: - - name: tool_name # Required: Tool identifier - description: Tool description # Optional: Human-readable description - input_schema: schemas/input.json # Required: JSON Schema for input - output_schema: schemas/output.json # Optional: JSON Schema for output -``` - -### Resources - -Static resources: - -```yaml -resources: - - uri: docs://readme # Required: Resource URI - name: README # Required: Display name - description: Project README # Optional - mime_type: text/markdown # Optional -``` - -Resource templates (dynamic URIs): - -```yaml -resources: - - uri_template: users://{id}/profile # Required: URI template - name: User Profile # Required - description: User profile data # Optional - mime_type: application/json # Optional - uri_params: # Parameters from template - - name: id - type: string - description: User ID -``` - -### Prompts - -```yaml -prompts: - - name: prompt_name # Required: Prompt identifier - description: Description # Optional - arguments: # Optional: Prompt arguments - - name: arg_name - description: Arg description - required: true -``` - -## Commands - -### `mcpgen init [name]` - -Initialize a new MCP server project with example configuration. - -```bash -mcpgen init my-server -``` - -### `mcpgen generate` - -Generate code from `mcpgen.yaml` configuration. - -```bash -mcpgen generate - -# Specify custom config file -mcpgen generate --config custom-config.yaml -``` - -### `mcpgen version` - -Print mcpgen version. - -```bash -mcpgen version -``` - -## How It Works - -1. **Configuration Loading**: mcpgen reads your `mcpgen.yaml` file -2. **Schema Loading**: JSON Schemas are loaded and `$ref` references resolved -3. **Type Generation**: Go structs are generated from JSON Schemas -4. **Server Generation**: MCP server boilerplate is generated with tool/resource/prompt registration -5. **Resolver Generation**: Handler stubs are generated (only if they don't exist) - -## MCP Primitives - -### Tools - -Tools let LLMs interact with external systems. Each tool has: -- **Name**: Unique identifier (alphanumeric, underscore, dash, dot) -- **Description**: What the tool does -- **Input Schema**: JSON Schema defining parameters (required) -- **Output Schema**: JSON Schema for result validation (optional) - -### Resources - -Resources provide context to LLMs via URIs: -- **Static Resources**: Fixed URI (e.g., `docs://readme`) -- **Resource Templates**: Dynamic URIs (e.g., `users://{id}/profile`) - -### Prompts - -Prompts are reusable templates for LLM interactions with optional arguments. - -## Comparison with gqlgen - -| Feature | gqlgen | mcpgen | -|---------|--------|--------| -| **Schema Language** | GraphQL SDL | JSON Schema | -| **Protocol** | GraphQL | MCP (JSON-RPC 2.0) | -| **Core Primitives** | Queries, Mutations, Subscriptions | Tools, Resources, Prompts | -| **Generation** | Resolvers, models | Handlers, models | -| **Schema-first** | āœ… | āœ… | -| **Preserve implementations** | āœ… | āœ… | -| **Type safety** | āœ… | āœ… | - -## Custom Type Mapping - -You can use your own Go types instead of generated ones, similar to gqlgen's model binding. - -### Using Schema Annotations (Recommended) - -Add `go.probo.inc/mcpgen/type` annotations in your JSON Schema: - -```yaml -components: - schemas: - # Use time.Time for timestamps - Timestamp: - type: string - format: date-time - go.probo.inc/mcpgen/type: time.Time - - # Use UUID package - UUID: - type: string - format: uuid - go.probo.inc/mcpgen/type: github.com/google/uuid.UUID - - # Use your own domain models - User: - type: object - properties: - id: - type: string - name: - type: string - go.probo.inc/mcpgen/type: github.com/myorg/models.User -``` - -When you reference these schemas, mcpgen will: -- Skip generating types for them -- Use your custom types instead -- Automatically add necessary imports - -See [docs/custom-types.md](docs/custom-types.md) for full documentation. - -## Examples - -See the `examples/` directory for complete working examples. - -## Development - -### Building - -```bash -go build -o mcpgen -``` - -### Testing - -```bash -go test ./... -``` - -## Contributing - -Contributions welcome. Please submit a Pull Request. - -## License - -MIT License - see LICENSE file for details. - -## Acknowledgments - -- Inspired by [gqlgen](https://github.com/99designs/gqlgen) -- Uses the official [Model Context Protocol Go SDK](https://github.com/modelcontextprotocol/go-sdk) diff --git a/third_party/mcpgen/THIRD_PARTY.md b/third_party/mcpgen/THIRD_PARTY.md deleted file mode 100644 index 8ce1b179a..000000000 --- a/third_party/mcpgen/THIRD_PARTY.md +++ /dev/null @@ -1,12 +0,0 @@ -# Vendored mcpgen - -This is a temporary fork of [getprobo/mcpgen](https://github.com/getprobo/mcpgen) -(module `go.probo.inc/mcpgen`) with tool annotation improvements: - -- `title` field on tools (emitted as `Tool.Title` and `ToolAnnotations.Title`) -- When `hints` are present, always emit annotations so write tools get - `readOnlyHint: false` and `destructiveHint: false`, distinguishing them from - deletes (`destructiveHint: true`) - -`go.mod` replaces `go.probo.inc/mcpgen` with this directory. Once the same -changes land upstream, drop the replace and delete this tree. diff --git a/third_party/mcpgen/go.mod b/third_party/mcpgen/go.mod deleted file mode 100644 index 7e3bf1bbe..000000000 --- a/third_party/mcpgen/go.mod +++ /dev/null @@ -1,21 +0,0 @@ -module go.probo.inc/mcpgen - -go 1.25.3 - -require ( - github.com/google/jsonschema-go v0.3.0 - github.com/modelcontextprotocol/go-sdk v1.1.0 - github.com/spf13/cobra v1.10.1 - github.com/stretchr/testify v1.11.1 - golang.org/x/mod v0.30.0 - gopkg.in/yaml.v3 v3.0.1 -) - -require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/spf13/pflag v1.0.9 // indirect - github.com/yosida95/uritemplate/v3 v3.0.2 // indirect - golang.org/x/oauth2 v0.30.0 // indirect -) diff --git a/third_party/mcpgen/go.sum b/third_party/mcpgen/go.sum deleted file mode 100644 index 076f147e6..000000000 --- a/third_party/mcpgen/go.sum +++ /dev/null @@ -1,32 +0,0 @@ -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q= -github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/modelcontextprotocol/go-sdk v1.1.0 h1:Qjayg53dnKC4UZ+792W21e4BpwEZBzwgRW6LrjLWSwA= -github.com/modelcontextprotocol/go-sdk v1.1.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= -github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= -github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= -github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= -github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/third_party/mcpgen/internal/codegen/generator.go b/third_party/mcpgen/internal/codegen/generator.go deleted file mode 100644 index 9920fee52..000000000 --- a/third_party/mcpgen/internal/codegen/generator.go +++ /dev/null @@ -1,1292 +0,0 @@ -package codegen - -import ( - "bytes" - "embed" - "encoding/json" - "fmt" - "go/format" - "os" - "path/filepath" - "sort" - "strings" - "text/template" - - "go.probo.inc/mcpgen/internal/config" - "go.probo.inc/mcpgen/internal/schema" - "golang.org/x/mod/modfile" -) - -//go:embed templates/*.gotpl -var templates embed.FS - -type Generator struct { - config *config.Config - spec *config.MCPSpec - schemaLoader *schema.Loader - typeGen *TypeGenerator -} - -func New(cfg *config.Config, spec *config.MCPSpec) *Generator { - typeGen := NewTypeGenerator() - - // Sort schema names for deterministic output - schemaNames := make([]string, 0, len(cfg.Models.Models)) - for schemaName := range cfg.Models.Models { - schemaNames = append(schemaNames, schemaName) - } - sort.Strings(schemaNames) - - for _, schemaName := range schemaNames { - typeMapping := cfg.Models.Models[schemaName] - customMapping := parseTypeMapping(typeMapping.Model) - typeGen.AddCustomMapping(schemaName, customMapping) - } - - return &Generator{ - config: cfg, - spec: spec, - schemaLoader: schema.NewLoader("."), - typeGen: typeGen, - } -} - -func (g *Generator) Generate() error { - if err := g.loadSchemas(); err != nil { - return fmt.Errorf("failed to load schemas: %w", err) - } - - if err := g.generateModels(); err != nil { - return fmt.Errorf("failed to generate models: %w", err) - } - - if err := g.generateServer(); err != nil { - return fmt.Errorf("failed to generate server: %w", err) - } - - if err := g.generateResolverStruct(); err != nil { - return fmt.Errorf("failed to generate resolver struct: %w", err) - } - - if err := g.generateResolverImplementations(); err != nil { - return fmt.Errorf("failed to generate resolver implementations: %w", err) - } - - return nil -} - -func (g *Generator) loadSchemas() error { - // Sort schema names for deterministic output - schemaNames := make([]string, 0, len(g.spec.Components.Schemas)) - for name := range g.spec.Components.Schemas { - schemaNames = append(schemaNames, name) - } - sort.Strings(schemaNames) - - for _, name := range schemaNames { - schema := g.spec.Components.Schemas[name] - if config.IsSchemaRef(schema) { - s, err := g.schemaLoader.Load(schema.Ref) - if err != nil { - return fmt.Errorf("failed to load schema %s: %w", name, err) - } - if goType := extractGoTypeAnnotation(s); goType != "" { - customMapping := parseTypeMapping(goType) - g.typeGen.AddCustomMapping(name, customMapping) - } - g.typeGen.AddSchema(name, s) - } else { - if goType := extractGoTypeAnnotation(schema); goType != "" { - customMapping := parseTypeMapping(goType) - g.typeGen.AddCustomMapping(name, customMapping) - } - g.typeGen.AddSchema(name, schema) - } - } - - for _, tool := range g.spec.Tools { - if tool.InputSchema != nil { - typeName := toPascalCase(tool.Name) + "Input" - handlerName := toHandlerName(tool.Name) - schemaVarName := handlerName + "ToolInputSchema" - - var resolvedSchema *config.Schema - if config.IsSchemaRef(tool.InputSchema) { - if len(tool.InputSchema.Ref) > 0 && tool.InputSchema.Ref[0] == '#' { - resolved, err := g.spec.ResolveSchemaRef(tool.InputSchema.Ref) - if err != nil { - return fmt.Errorf("failed to resolve input schema ref for tool %s: %w", tool.Name, err) - } - resolvedSchema = resolved - g.typeGen.AddSchema(typeName, resolvedSchema) - } else { - s, err := g.schemaLoader.Load(tool.InputSchema.Ref) - if err != nil { - return fmt.Errorf("failed to load input schema for tool %s: %w", tool.Name, err) - } - resolvedSchema = s - g.typeGen.AddSchema(typeName, s) - } - } else { - resolvedSchema = tool.InputSchema - g.typeGen.AddSchema(typeName, tool.InputSchema) - } - - if resolvedSchema != nil { - fullyResolvedSchema, err := g.resolveAllRefs(resolvedSchema) - if err != nil { - return fmt.Errorf("failed to fully resolve schema for tool %s: %w", tool.Name, err) - } - schemaJSON, err := json.Marshal(fullyResolvedSchema) - if err == nil { - g.typeGen.AddSchemaVar(schemaVarName, string(schemaJSON)) - } - } - } - - // Process output schema if present - if tool.OutputSchema != nil { - typeName := toPascalCase(tool.Name) + "Output" - handlerName := toHandlerName(tool.Name) - schemaVarName := handlerName + "ToolOutputSchema" - - var resolvedSchema *config.Schema - if config.IsSchemaRef(tool.OutputSchema) { - if len(tool.OutputSchema.Ref) > 0 && tool.OutputSchema.Ref[0] == '#' { - resolved, err := g.spec.ResolveSchemaRef(tool.OutputSchema.Ref) - if err != nil { - return fmt.Errorf("failed to resolve output schema ref for tool %s: %w", tool.Name, err) - } - resolvedSchema = resolved - g.typeGen.AddSchema(typeName, resolvedSchema) - } else { - s, err := g.schemaLoader.Load(tool.OutputSchema.Ref) - if err != nil { - return fmt.Errorf("failed to load output schema for tool %s: %w", tool.Name, err) - } - resolvedSchema = s - g.typeGen.AddSchema(typeName, s) - } - } else { - resolvedSchema = tool.OutputSchema - g.typeGen.AddSchema(typeName, tool.OutputSchema) - } - - if resolvedSchema != nil { - fullyResolvedSchema, err := g.resolveAllRefs(resolvedSchema) - if err != nil { - return fmt.Errorf("failed to fully resolve schema for tool %s: %w", tool.Name, err) - } - schemaJSON, err := json.Marshal(fullyResolvedSchema) - if err == nil { - g.typeGen.AddSchemaVar(schemaVarName, string(schemaJSON)) - } - } - } - } - - for _, resource := range g.spec.Resources { - if resource.Schema != nil { - typeName := toPascalCase(resource.Name) + "Content" - if config.IsSchemaRef(resource.Schema) { - if len(resource.Schema.Ref) > 0 && resource.Schema.Ref[0] == '#' { - resolvedSchema, err := g.spec.ResolveSchemaRef(resource.Schema.Ref) - if err != nil { - return fmt.Errorf("failed to resolve schema ref for resource %s: %w", resource.Name, err) - } - g.typeGen.AddSchema(typeName, resolvedSchema) - continue - } - s, err := g.schemaLoader.Load(resource.Schema.Ref) - if err != nil { - return fmt.Errorf("failed to load schema for resource %s: %w", resource.Name, err) - } - g.typeGen.AddSchema(typeName, s) - } else { - g.typeGen.AddSchema(typeName, resource.Schema) - } - } - } - - // Generate typed argument structs for prompts - for _, prompt := range g.spec.Prompts { - if len(prompt.Arguments) > 0 { - typeName := toPascalCase(prompt.Name) + "Args" - - // Create a schema from the prompt arguments - argSchema := &config.Schema{ - Type: "object", - Properties: make(map[string]*config.Schema), - Required: []string{}, - } - - for _, arg := range prompt.Arguments { - argSchema.Properties[arg.Name] = &config.Schema{ - Type: "string", - Description: arg.Description, - } - if arg.Required { - argSchema.Required = append(argSchema.Required, arg.Name) - } - } - - g.typeGen.AddSchema(typeName, argSchema) - } - } - - return nil -} - -func (g *Generator) resolveAllRefs(s *config.Schema) (*config.Schema, error) { - if s == nil { - return nil, nil - } - - if config.IsSchemaRef(s) { - if len(s.Ref) > 0 && s.Ref[0] == '#' { - resolved, err := g.spec.ResolveSchemaRef(s.Ref) - if err != nil { - return nil, err - } - return g.resolveAllRefs(resolved) - } - return s, nil - } - - result := &config.Schema{ - Type: s.Type, - Types: s.Types, - Format: s.Format, - Description: s.Description, - Default: s.Default, - Enum: s.Enum, - Title: s.Title, - Required: s.Required, - ReadOnly: s.ReadOnly, - WriteOnly: s.WriteOnly, - Deprecated: s.Deprecated, - Minimum: s.Minimum, - Maximum: s.Maximum, - ExclusiveMinimum: s.ExclusiveMinimum, - ExclusiveMaximum: s.ExclusiveMaximum, - MinLength: s.MinLength, - MaxLength: s.MaxLength, - Pattern: s.Pattern, - MinItems: s.MinItems, - MaxItems: s.MaxItems, - UniqueItems: s.UniqueItems, - MinProperties: s.MinProperties, - MaxProperties: s.MaxProperties, - } - - if len(s.Properties) > 0 { - result.Properties = make(map[string]*config.Schema) - // Sort property names for deterministic output - propNames := make([]string, 0, len(s.Properties)) - for key := range s.Properties { - propNames = append(propNames, key) - } - sort.Strings(propNames) - for _, key := range propNames { - propSchema := s.Properties[key] - resolvedProp, err := g.resolveAllRefs(propSchema) - if err != nil { - return nil, fmt.Errorf("failed to resolve property %s: %w", key, err) - } - result.Properties[key] = resolvedProp - } - } - - if s.Items != nil { - resolvedItems, err := g.resolveAllRefs(s.Items) - if err != nil { - return nil, fmt.Errorf("failed to resolve items: %w", err) - } - result.Items = resolvedItems - } - - if len(s.AnyOf) > 0 { - result.AnyOf = make([]*config.Schema, len(s.AnyOf)) - for i, schema := range s.AnyOf { - resolvedSchema, err := g.resolveAllRefs(schema) - if err != nil { - return nil, fmt.Errorf("failed to resolve anyOf[%d]: %w", i, err) - } - result.AnyOf[i] = resolvedSchema - } - } - - if len(s.AllOf) > 0 { - result.AllOf = make([]*config.Schema, len(s.AllOf)) - for i, schema := range s.AllOf { - resolvedSchema, err := g.resolveAllRefs(schema) - if err != nil { - return nil, fmt.Errorf("failed to resolve allOf[%d]: %w", i, err) - } - result.AllOf[i] = resolvedSchema - } - } - - if len(s.OneOf) > 0 { - result.OneOf = make([]*config.Schema, len(s.OneOf)) - for i, schema := range s.OneOf { - resolvedSchema, err := g.resolveAllRefs(schema) - if err != nil { - return nil, fmt.Errorf("failed to resolve oneOf[%d]: %w", i, err) - } - result.OneOf[i] = resolvedSchema - } - } - - if s.Not != nil { - resolvedNot, err := g.resolveAllRefs(s.Not) - if err != nil { - return nil, fmt.Errorf("failed to resolve not: %w", err) - } - result.Not = resolvedNot - } - - if s.AdditionalProperties != nil { - resolvedAdditional, err := g.resolveAllRefs(s.AdditionalProperties) - if err != nil { - return nil, fmt.Errorf("failed to resolve additionalProperties: %w", err) - } - result.AdditionalProperties = resolvedAdditional - } - - if len(s.PatternProperties) > 0 { - result.PatternProperties = make(map[string]*config.Schema) - // Sort pattern names for deterministic output - patterns := make([]string, 0, len(s.PatternProperties)) - for pattern := range s.PatternProperties { - patterns = append(patterns, pattern) - } - sort.Strings(patterns) - for _, pattern := range patterns { - patternSchema := s.PatternProperties[pattern] - resolvedPattern, err := g.resolveAllRefs(patternSchema) - if err != nil { - return nil, fmt.Errorf("failed to resolve patternProperties[%s]: %w", pattern, err) - } - result.PatternProperties[pattern] = resolvedPattern - } - } - - return result, nil -} - -func toPascalCase(s string) string { - parts := strings.FieldsFunc(s, func(r rune) bool { - return r == '_' || r == '-' || r == ' ' - }) - for i, part := range parts { - if len(part) > 0 { - parts[i] = strings.ToUpper(part[:1]) + part[1:] - } - } - return strings.Join(parts, "") -} - -func (g *Generator) generateModels() error { - code, err := g.typeGen.Generate(g.config.Model.Package) - if err != nil { - return err - } - - modelsFile := "models.go" - if g.config.Model.Filename != "" { - modelsFile = g.config.Model.Filename - } - modelsPath := filepath.Join(g.config.Output, modelsFile) - - dir := filepath.Dir(modelsPath) - if err := os.MkdirAll(dir, 0755); err != nil { - return fmt.Errorf("failed to create directory: %w", err) - } - - if err := os.WriteFile(modelsPath, code, 0644); err != nil { - return fmt.Errorf("failed to write models file: %w", err) - } - - fmt.Printf("Generated models: %s\n", modelsPath) - return nil -} - -func (g *Generator) generateServer() error { - tmpl, err := template.ParseFS(templates, "templates/server.gotpl") - if err != nil { - return fmt.Errorf("failed to parse server template: %w", err) - } - - data := g.buildServerTemplateData() - - var buf bytes.Buffer - if err := tmpl.Execute(&buf, data); err != nil { - return fmt.Errorf("failed to execute server template: %w", err) - } - - formatted, err := format.Source(buf.Bytes()) - if err != nil { - return fmt.Errorf("failed to format server code: %w\n%s", err, buf.String()) - } - - serverFile := "server.go" - if g.config.Exec.Filename != "" { - serverFile = g.config.Exec.Filename - } - serverPath := filepath.Join(g.config.Output, serverFile) - - dir := filepath.Dir(serverPath) - if err := os.MkdirAll(dir, 0755); err != nil { - return fmt.Errorf("failed to create directory: %w", err) - } - - if err := os.WriteFile(serverPath, formatted, 0644); err != nil { - return fmt.Errorf("failed to write server file: %w", err) - } - - fmt.Printf("Generated server: %s\n", serverPath) - return nil -} - -// generateResolverStruct creates the main resolver.go file with the Resolver struct -// This file is only generated once and users can edit it freely -func (g *Generator) generateResolverStruct() error { - resolverFile := filepath.Join(g.config.Output, "resolver.go") - - // Only generate if file doesn't exist - if _, err := os.Stat(resolverFile); err == nil { - fmt.Printf("Resolver struct already exists, skipping: %s\n", resolverFile) - return nil - } - - tmpl, err := template.ParseFS(templates, "templates/resolver_struct.gotpl") - if err != nil { - return fmt.Errorf("failed to parse resolver_struct template: %w", err) - } - - data := map[string]interface{}{ - "Package": g.config.Resolver.Package, - "ResolverType": g.config.Resolver.Type, - } - - var buf bytes.Buffer - if err := tmpl.Execute(&buf, data); err != nil { - return fmt.Errorf("failed to execute resolver_struct template: %w", err) - } - - formatted, err := format.Source(buf.Bytes()) - if err != nil { - return fmt.Errorf("failed to format resolver struct code: %w\n%s", err, buf.String()) - } - - if err := os.WriteFile(resolverFile, formatted, 0644); err != nil { - return fmt.Errorf("failed to write resolver struct file: %w", err) - } - - fmt.Printf("Generated resolver struct: %s\n", resolverFile) - return nil -} - -// generateResolverImplementations creates/updates schema.resolvers.go with tool/prompt/resource implementations -func (g *Generator) generateResolverImplementations() error { - resolverFile := filepath.Join(g.config.Output, "schema.resolvers.go") - - fileExists := false - if _, err := os.Stat(resolverFile); err == nil { - fileExists = true - } - - // If file doesn't exist, generate from template (initial generation) - if !fileExists { - return g.generateResolverFromTemplate(resolverFile) - } - - // File exists and preserve is enabled - do incremental update (gqlgen-style) - if g.config.Resolver.Preserve { - return g.updateResolverIncremental(resolverFile) - } - - // File exists but preserve is disabled - regenerate completely - return g.generateResolverFromTemplate(resolverFile) -} - -func (g *Generator) generateResolverFromTemplate(resolverFile string) error { - tmpl, err := template.ParseFS(templates, "templates/resolver.gotpl") - if err != nil { - return fmt.Errorf("failed to parse resolver template: %w", err) - } - - data := g.buildResolverTemplateData() - - var buf bytes.Buffer - if err := tmpl.Execute(&buf, data); err != nil { - return fmt.Errorf("failed to execute resolver template: %w", err) - } - - formatted, err := format.Source(buf.Bytes()) - if err != nil { - return fmt.Errorf("failed to format resolver code: %w\n%s", err, buf.String()) - } - - if err := os.WriteFile(resolverFile, formatted, 0644); err != nil { - return fmt.Errorf("failed to write resolver file: %w", err) - } - - fmt.Printf("Generated resolver implementations: %s\n", resolverFile) - return nil -} - -func (g *Generator) updateResolverIncremental(resolverFile string) error { - parser, err := NewResolverParser(resolverFile) - if err != nil { - return fmt.Errorf("failed to parse existing resolver: %w", err) - } - - existingHandlers, err := parser.ExtractHandlers(g.config.Resolver.Type) - if err != nil { - return fmt.Errorf("failed to extract handlers: %w", err) - } - - requiredHandlers := g.getRequiredHandlerNames() - - // Identify which handlers are new - newHandlers := []string{} - for _, required := range requiredHandlers { - if _, exists := existingHandlers[required]; !exists { - newHandlers = append(newHandlers, required) - } - } - - // Identify orphaned handlers (exist in file but not in spec, excluding already orphaned ones) - // First, get the list of handlers that were already in the orphaned section - previouslyOrphanedHandlers := extractOrphanedHandlerNames(resolverFile) - - // Identify which handlers are orphaned (not in required list and not already in orphaned section) - currentlyOrphanedHandlers := []string{} - for handlerName := range existingHandlers { - isRequired := false - for _, required := range requiredHandlers { - if required == handlerName { - isRequired = true - break - } - } - // Only mark as newly orphaned if not required and not already orphaned - if !isRequired && !contains(previouslyOrphanedHandlers, handlerName) { - currentlyOrphanedHandlers = append(currentlyOrphanedHandlers, handlerName) - } - } - - // Check if any previously orphaned handlers are now required (should be removed from orphaned) - orphanedHandlersRemoved := []string{} - for _, orphanedName := range previouslyOrphanedHandlers { - if contains(requiredHandlers, orphanedName) { - orphanedHandlersRemoved = append(orphanedHandlersRemoved, orphanedName) - } - } - - // Mark handlers as orphaned for formatting - IdentifyOrphanedHandlers(existingHandlers, requiredHandlers) - orphanedHandlers := FormatOrphanedHandlers(existingHandlers) - - // If nothing changed, skip update - if len(newHandlers) == 0 && len(currentlyOrphanedHandlers) == 0 && len(orphanedHandlersRemoved) == 0 { - fmt.Printf("Resolver is up to date, skipping: %s\n", resolverFile) - return nil - } - - // Generate code for new handlers only - newHandlersCode, err := g.generateNewHandlersCode(newHandlers) - if err != nil { - return fmt.Errorf("failed to generate new handlers: %w", err) - } - - // Read the existing file - content, err := os.ReadFile(resolverFile) - if err != nil { - return fmt.Errorf("failed to read resolver file: %w", err) - } - - // Remove any existing orphaned handlers section - contentStr := string(content) - if idx := strings.Index(contentStr, "\n// ==============================================================================\n// Orphaned Handlers\n"); idx != -1 { - contentStr = contentStr[:idx] - } - - // Build final content: existing code + new handlers + orphaned section - var buf bytes.Buffer - buf.WriteString(contentStr) - - if newHandlersCode != "" { - buf.WriteString("\n") - buf.WriteString(newHandlersCode) - } - - if orphanedHandlers != "" { - buf.WriteString(orphanedHandlers) - } - - // Format the final code - formatted, err := format.Source(buf.Bytes()) - if err != nil { - return fmt.Errorf("failed to format resolver code: %w\n%s", err, buf.String()) - } - - if err := os.WriteFile(resolverFile, formatted, 0644); err != nil { - return fmt.Errorf("failed to write resolver file: %w", err) - } - - // Build status message - var updates []string - if len(newHandlers) > 0 { - updates = append(updates, fmt.Sprintf("added %d new", len(newHandlers))) - } - if len(currentlyOrphanedHandlers) > 0 { - updates = append(updates, fmt.Sprintf("orphaned %d", len(currentlyOrphanedHandlers))) - } - if len(orphanedHandlersRemoved) > 0 { - updates = append(updates, fmt.Sprintf("restored %d from orphaned", len(orphanedHandlersRemoved))) - } - - fmt.Printf("Updated resolver: %s: %s\n", strings.Join(updates, ", "), resolverFile) - - return nil -} - -func countOrphanedHandlers(orphanedCode string) int { - return strings.Count(orphanedCode, "// Orphaned:") -} - -// extractOrphanedHandlerNames reads the orphaned section and returns list of handler names -func extractOrphanedHandlerNames(resolverFile string) []string { - content, err := os.ReadFile(resolverFile) - if err != nil { - return nil - } - - contentStr := string(content) - orphanedSectionStart := strings.Index(contentStr, "\n// ==============================================================================\n// Orphaned Handlers\n") - if orphanedSectionStart == -1 { - return nil - } - - orphanedSection := contentStr[orphanedSectionStart:] - var names []string - - // Find all "// Orphaned: " lines - lines := strings.Split(orphanedSection, "\n") - for _, line := range lines { - if strings.HasPrefix(line, "// Orphaned: ") { - name := strings.TrimPrefix(line, "// Orphaned: ") - names = append(names, name) - } - } - - return names -} - -func contains(slice []string, item string) bool { - for _, s := range slice { - if s == item { - return true - } - } - return false -} - -func (g *Generator) generateNewHandlersCode(handlerNames []string) (string, error) { - if len(handlerNames) == 0 { - return "", nil - } - - // Parse the resolver template to extract individual handler templates - tmpl, err := template.ParseFS(templates, "templates/resolver.gotpl") - if err != nil { - return "", fmt.Errorf("failed to parse resolver template: %w", err) - } - - // Build template data with only the new handlers - data := g.buildResolverTemplateData() - - // Filter to only include new handlers - handlerSet := make(map[string]bool) - for _, name := range handlerNames { - handlerSet[name] = true - } - - // Filter tools (HandlerName in data + "Tool" suffix should match required names) - if tools, ok := data["Tools"].([]map[string]interface{}); ok { - filteredTools := []map[string]interface{}{} - for _, tool := range tools { - if handlerName, ok := tool["HandlerName"].(string); ok { - if handlerSet[handlerName+"Tool"] { - filteredTools = append(filteredTools, tool) - } - } - } - data["Tools"] = filteredTools - } - - // Filter resources (HandlerName in data + "Resource" suffix should match required names) - if resources, ok := data["Resources"].([]map[string]interface{}); ok { - filteredResources := []map[string]interface{}{} - for _, resource := range resources { - if handlerName, ok := resource["HandlerName"].(string); ok { - if handlerSet[handlerName+"Resource"] { - filteredResources = append(filteredResources, resource) - } - } - } - data["Resources"] = filteredResources - data["HasResources"] = len(filteredResources) > 0 - } - - // Filter prompts (HandlerName in data + "Prompt" suffix should match required names) - if prompts, ok := data["Prompts"].([]map[string]interface{}); ok { - filteredPrompts := []map[string]interface{}{} - for _, prompt := range prompts { - if handlerName, ok := prompt["HandlerName"].(string); ok { - if handlerSet[handlerName+"Prompt"] { - filteredPrompts = append(filteredPrompts, prompt) - } - } - } - data["Prompts"] = filteredPrompts - data["HasPrompts"] = len(filteredPrompts) > 0 - } - - // Generate only handler methods (not the full file structure) - // NOTE: Must match the naming in resolver.gotpl template - handlersOnlyTmpl, err := template.New("handlers").Parse(` -{{- range .Tools }} - -{{- if .HasInputType }} -func (r *{{ $.ResolverType }}) {{ .HandlerName }}Tool(ctx context.Context, req *mcp.CallToolRequest, input *{{ .InputType }}) (*mcp.CallToolResult, {{ if .HasOutputType }}{{ .OutputType }}{{ else }}map[string]any{{ end }}, error) { - return nil, {{ if .HasOutputType }}{{ .OutputType }}{}{{ else }}nil{{ end }}, fmt.Errorf("{{ .Name }} not implemented") -} -{{- else }} -func (r *{{ $.ResolverType }}) {{ .HandlerName }}Tool(ctx context.Context, req *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, {{ if .HasOutputType }}{{ .OutputType }}{{ else }}map[string]any{{ end }}, error) { - return nil, {{ if .HasOutputType }}{{ .OutputType }}{}{{ else }}nil{{ end }}, fmt.Errorf("{{ .Name }} not implemented") -} -{{- end }} -{{- end }} - -{{- range .Resources }} - -func (r *{{ $.ResolverType }}) {{ .HandlerName }}Resource(ctx context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { - return nil, fmt.Errorf("{{ .Name }} not implemented") -} -{{- end }} - -{{- range .Prompts }} - -{{- if .HasArgsType }} -func (r *{{ $.ResolverType }}) {{ .HandlerName }}Prompt(ctx context.Context, req *mcp.GetPromptRequest, args {{ .ArgsType }}) (*mcp.GetPromptResult, error) { - return nil, fmt.Errorf("{{ .Name }} not implemented") -} -{{- else }} -func (r *{{ $.ResolverType }}) {{ .HandlerName }}Prompt(ctx context.Context, req *mcp.GetPromptRequest, args map[string]string) (*mcp.GetPromptResult, error) { - return nil, fmt.Errorf("{{ .Name }} not implemented") -} -{{- end }} -{{- end }} -`) - if err != nil { - return "", fmt.Errorf("failed to create handlers template: %w", err) - } - - var buf bytes.Buffer - if err := handlersOnlyTmpl.Execute(&buf, data); err != nil { - return "", fmt.Errorf("failed to execute handlers template: %w", err) - } - - _ = tmpl // Keep using template for future enhancements - return buf.String(), nil -} - -func (g *Generator) getRequiredHandlerNames() []string { - var names []string - - for _, tool := range g.spec.Tools { - names = append(names, toHandlerName(tool.Name)+"Tool") - } - - for _, resource := range g.spec.Resources { - names = append(names, toHandlerName(resource.Name)+"Resource") - } - - for _, prompt := range g.spec.Prompts { - names = append(names, toHandlerName(prompt.Name)+"Prompt") - } - - return names -} - -func (g *Generator) buildServerTemplateData() map[string]interface{} { - // Compute type prefix if model package is different from exec package - modelPackage := g.config.Model.Package - execPackage := g.config.Exec.Package - typePrefix := "" - modelImportPath := "" - - // Server needs to import models if they're in a different package - if modelPackage != execPackage { - parts := strings.Split(modelPackage, "/") - typePrefix = parts[len(parts)-1] + "." - - // Compute the full import path for the model package - modelImportPath = g.computeModelImportPath() - } - - tools := make([]map[string]interface{}, 0, len(g.spec.Tools)) - hasTypedTools := false - for _, tool := range g.spec.Tools { - toolData := map[string]interface{}{ - "Name": tool.Name, - "Title": tool.Title, - "Description": tool.Description, - "HandlerName": toHandlerName(tool.Name), - } - - // Add hints if present - if tool.Hints != nil { - toolData["HasHints"] = true - toolData["Readonly"] = tool.Hints.Readonly - toolData["Destructive"] = tool.Hints.Destructive - toolData["Idempotent"] = tool.Hints.Idempotent - toolData["OpenWorld"] = tool.Hints.OpenWorld - } - - // Add input type information and schema code - if tool.InputSchema != nil { - inputTypeName := typePrefix + toPascalCase(tool.Name) + "Input" - toolData["InputType"] = inputTypeName - toolData["HasInputType"] = true - - // Add schema variable name with proper prefix - schemaVarName := typePrefix + toHandlerName(tool.Name) + "ToolInputSchema" - toolData["InputSchemaVar"] = schemaVarName - - resolvedSchema := tool.InputSchema - if config.IsSchemaRef(tool.InputSchema) && len(tool.InputSchema.Ref) > 0 && tool.InputSchema.Ref[0] == '#' { - resolved, err := g.spec.ResolveSchemaRef(tool.InputSchema.Ref) - if err == nil { - resolvedSchema = resolved - } - } - - schemaCode := g.generateSchemaCode(resolvedSchema) - toolData["InputSchemaCode"] = schemaCode - - hasTypedTools = true - } - - // Add output type information and schema code - if tool.OutputSchema != nil { - outputTypeName := typePrefix + toPascalCase(tool.Name) + "Output" - toolData["OutputType"] = outputTypeName - toolData["HasOutputType"] = true - - // Add schema variable name with proper prefix - schemaVarName := typePrefix + toHandlerName(tool.Name) + "ToolOutputSchema" - toolData["OutputSchemaVar"] = schemaVarName - - resolvedSchema := tool.OutputSchema - if config.IsSchemaRef(tool.OutputSchema) && len(tool.OutputSchema.Ref) > 0 && tool.OutputSchema.Ref[0] == '#' { - resolved, err := g.spec.ResolveSchemaRef(tool.OutputSchema.Ref) - if err == nil { - resolvedSchema = resolved - } - } - - schemaCode := g.generateSchemaCode(resolvedSchema) - toolData["OutputSchemaCode"] = schemaCode - } - - tools = append(tools, toolData) - } - - resources := make([]map[string]interface{}, 0, len(g.spec.Resources)) - for _, resource := range g.spec.Resources { - resData := map[string]interface{}{ - "Name": resource.Name, - "Description": resource.Description, - "HandlerName": toHandlerName(resource.Name), - "MimeType": resource.MimeType, - "Readonly": resource.Readonly, - } - - if resource.URI != "" { - resData["URI"] = resource.URI - } else if resource.URITemplate != "" { - resData["URITemplate"] = resource.URITemplate - params := extractURIParams(resource.URITemplate) - resData["URIParams"] = params - } - - resources = append(resources, resData) - } - - prompts := make([]map[string]interface{}, 0, len(g.spec.Prompts)) - for _, prompt := range g.spec.Prompts { - args := make([]map[string]interface{}, 0, len(prompt.Arguments)) - for _, arg := range prompt.Arguments { - args = append(args, map[string]interface{}{ - "Name": arg.Name, - "Description": arg.Description, - "Required": arg.Required, - }) - } - - promptData := map[string]interface{}{ - "Name": prompt.Name, - "Description": prompt.Description, - "HandlerName": toHandlerName(prompt.Name), - "Arguments": args, - } - - // Add args type if there are arguments - if len(prompt.Arguments) > 0 { - argsTypeName := typePrefix + toPascalCase(prompt.Name) + "Args" - promptData["ArgsType"] = argsTypeName - promptData["HasArgsType"] = true - } - - prompts = append(prompts, promptData) - } - - data := map[string]interface{}{ - "Package": g.config.Exec.Package, - "ServerName": g.spec.Info.Title, - "ServerVersion": g.spec.Info.Version, - "ResolverType": g.config.Resolver.Type, - "Tools": tools, - "Resources": resources, - "Prompts": prompts, - "HasResources": len(resources) > 0, - "HasPrompts": len(prompts) > 0, - "HasTypedTools": hasTypedTools, - } - - // Add imports if packages are different from exec package - var imports []map[string]string - if modelPackage != execPackage && modelImportPath != "" { - imports = append(imports, map[string]string{ - "Path": modelImportPath, - "Alias": "", - }) - } - // Never import resolver package - use interfaces to avoid circular imports - if len(imports) > 0 { - data["Imports"] = imports - } - - return data -} - -func (g *Generator) buildResolverTemplateData() map[string]interface{} { - // Resolver template data is similar to server template data, but uses resolver package - modelPackage := g.config.Model.Package - resolverPackage := g.config.Resolver.Package - typePrefix := "" - modelImportPath := "" - - // Resolver needs to import models if they're in a different package - if modelPackage != resolverPackage { - parts := strings.Split(modelPackage, "/") - typePrefix = parts[len(parts)-1] + "." - - // Compute the full import path for the model package - modelImportPath = g.computeModelImportPath() - } - - tools := make([]map[string]interface{}, 0, len(g.spec.Tools)) - hasTypedTools := false - for _, tool := range g.spec.Tools { - toolData := map[string]interface{}{ - "Name": tool.Name, - "Title": tool.Title, - "Description": tool.Description, - "HandlerName": toHandlerName(tool.Name), - } - - // Add hints if present - if tool.Hints != nil { - toolData["HasHints"] = true - toolData["Readonly"] = tool.Hints.Readonly - toolData["Destructive"] = tool.Hints.Destructive - toolData["Idempotent"] = tool.Hints.Idempotent - toolData["OpenWorld"] = tool.Hints.OpenWorld - } - - // Add input type information - if tool.InputSchema != nil { - inputTypeName := typePrefix + toPascalCase(tool.Name) + "Input" - toolData["InputType"] = inputTypeName - toolData["HasInputType"] = true - hasTypedTools = true - } - - // Add output type information - if tool.OutputSchema != nil { - outputTypeName := typePrefix + toPascalCase(tool.Name) + "Output" - toolData["OutputType"] = outputTypeName - toolData["HasOutputType"] = true - } - - tools = append(tools, toolData) - } - - resources := make([]map[string]interface{}, 0, len(g.spec.Resources)) - for _, resource := range g.spec.Resources { - resData := map[string]interface{}{ - "Name": resource.Name, - "Description": resource.Description, - "HandlerName": toHandlerName(resource.Name), - "MimeType": resource.MimeType, - "Readonly": resource.Readonly, - } - - if resource.URI != "" { - resData["URI"] = resource.URI - } else if resource.URITemplate != "" { - resData["URITemplate"] = resource.URITemplate - params := extractURIParams(resource.URITemplate) - resData["URIParams"] = params - } - - resources = append(resources, resData) - } - - prompts := make([]map[string]interface{}, 0, len(g.spec.Prompts)) - for _, prompt := range g.spec.Prompts { - args := make([]map[string]interface{}, 0, len(prompt.Arguments)) - for _, arg := range prompt.Arguments { - args = append(args, map[string]interface{}{ - "Name": arg.Name, - "Description": arg.Description, - "Required": arg.Required, - }) - } - - promptData := map[string]interface{}{ - "Name": prompt.Name, - "Description": prompt.Description, - "HandlerName": toHandlerName(prompt.Name), - "Arguments": args, - } - - // Add args type if there are arguments - if len(prompt.Arguments) > 0 { - argsTypeName := typePrefix + toPascalCase(prompt.Name) + "Args" - promptData["ArgsType"] = argsTypeName - promptData["HasArgsType"] = true - } - - prompts = append(prompts, promptData) - } - - data := map[string]interface{}{ - "Package": g.config.Resolver.Package, - "ServerName": g.spec.Info.Title, - "ServerVersion": g.spec.Info.Version, - "ResolverType": g.config.Resolver.Type, - "Tools": tools, - "Resources": resources, - "Prompts": prompts, - "HasResources": len(resources) > 0, - "HasPrompts": len(prompts) > 0, - "HasTypedTools": hasTypedTools, - } - - // Add model package import if different from resolver package - if modelPackage != resolverPackage && modelImportPath != "" { - var imports []map[string]string - imports = append(imports, map[string]string{ - "Path": modelImportPath, - "Alias": "", - }) - data["Imports"] = imports - } - - return data -} - -// computeModelImportPath computes the full import path for the model package -func (g *Generator) computeModelImportPath() string { - return g.computeImportPath(g.config.Model.Package, g.config.Model.Filename) -} - -// computeResolverImportPath computes the full import path for the resolver package -func (g *Generator) computeResolverImportPath() string { - return g.computeImportPath(g.config.Resolver.Package, g.config.Resolver.Filename) -} - -// computeImportPath computes the full import path for a package -func (g *Generator) computeImportPath(pkgName, filename string) string { - // If the package is already a full path (contains slashes), use it as-is - if strings.Contains(pkgName, "/") { - return pkgName - } - - // Make output path absolute - absOutput, err := filepath.Abs(g.config.Output) - if err != nil { - return pkgName - } - - // Find the closest go.mod to the output directory - modulePath, moduleRoot, err := findClosestGoMod(absOutput) - if err != nil { - // If we can't read go.mod, fall back to using the package name directly - return pkgName - } - - // Compute the relative path from module root to output directory - relPath, err := filepath.Rel(moduleRoot, absOutput) - if err != nil { - // If we can't compute relative path, fall back to package name - return pkgName - } - - // Compute the import path based on module + relative path + filename dir - // Example: demo + generated + types = demo/generated/types - fileDir := filepath.Dir(filename) - if fileDir == "." { - // If filename has no directory component, the files are in the output root - // Import path should be module + output relative path - return filepath.ToSlash(filepath.Join(modulePath, relPath)) - } - - // Otherwise, use the directory from the filename - return filepath.ToSlash(filepath.Join(modulePath, relPath, fileDir)) -} - -// findClosestGoMod finds the closest go.mod file by walking up from the given directory -// Returns the module path and the directory containing go.mod -func findClosestGoMod(startDir string) (modulePath string, moduleRoot string, err error) { - // Make startDir absolute - absDir, err := filepath.Abs(startDir) - if err != nil { - return "", "", err - } - - // Walk up the directory tree looking for go.mod - currentDir := absDir - for { - goModPath := filepath.Join(currentDir, "go.mod") - if _, err := os.Stat(goModPath); err == nil { - // Found go.mod, parse it using the official modfile package - data, err := os.ReadFile(goModPath) - if err != nil { - return "", "", err - } - - parsed, err := modfile.Parse(goModPath, data, nil) - if err != nil { - return "", "", fmt.Errorf("failed to parse %s: %w", goModPath, err) - } - - if parsed.Module == nil || parsed.Module.Mod.Path == "" { - return "", "", fmt.Errorf("no module directive found in %s", goModPath) - } - - return parsed.Module.Mod.Path, currentDir, nil - } - - // Move up one directory - parent := filepath.Dir(currentDir) - if parent == currentDir { - // Reached the root directory - return "", "", fmt.Errorf("no go.mod found in any parent directory of %s", absDir) - } - currentDir = parent - } -} - -func toHandlerName(name string) string { - parts := strings.FieldsFunc(name, func(r rune) bool { - return r == '_' || r == '-' || r == ' ' - }) - - for i, part := range parts { - if len(part) > 0 { - parts[i] = strings.ToUpper(part[:1]) + part[1:] - } - } - - return strings.Join(parts, "") -} - -func (g *Generator) generateSchemaCode(s *config.Schema) string { - schemaJSON, err := json.Marshal(s) - if err != nil { - return "nil" - } - - return fmt.Sprintf("mustUnmarshalSchema(`%s`)", string(schemaJSON)) -} - -func extractURIParams(uriTemplate string) []map[string]interface{} { - var params []map[string]interface{} - start := -1 - for i, ch := range uriTemplate { - if ch == '{' { - start = i + 1 - } else if ch == '}' && start >= 0 { - paramName := uriTemplate[start:i] - params = append(params, map[string]interface{}{ - "Name": paramName, - "Description": fmt.Sprintf("Parameter from URI template: %s", paramName), - }) - start = -1 - } - } - - return params -} - -func extractGoTypeAnnotation(s *config.Schema) string { - if s == nil || s.Extra == nil { - return "" - } - - if goType, ok := s.Extra["go.probo.inc/mcpgen/type"]; ok { - if goTypeStr, ok := goType.(string); ok { - return goTypeStr - } - } - - return "" -} - -func parseTypeMapping(modelStr string) *CustomTypeMapping { - mapping := &CustomTypeMapping{ - GoType: modelStr, - } - - if strings.Contains(modelStr, "/") { - parts := strings.Split(modelStr, ".") - if len(parts) >= 2 { - typeName := parts[len(parts)-1] - importPath := strings.TrimSuffix(modelStr, "."+typeName) - mapping.GoType = typeName - mapping.ImportPath = importPath - - if strings.Contains(importPath, "/") { - pkgParts := strings.Split(importPath, "/") - pkgAlias := pkgParts[len(pkgParts)-1] - mapping.GoType = pkgAlias + "." + typeName - } - } - } else if strings.Contains(modelStr, ".") { - parts := strings.Split(modelStr, ".") - if len(parts) == 2 { - mapping.ImportPath = parts[0] - mapping.GoType = modelStr - } - } - - return mapping -} diff --git a/third_party/mcpgen/internal/codegen/generator_test.go b/third_party/mcpgen/internal/codegen/generator_test.go deleted file mode 100644 index f383778f1..000000000 --- a/third_party/mcpgen/internal/codegen/generator_test.go +++ /dev/null @@ -1,1387 +0,0 @@ -package codegen - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/assert" - - "go.probo.inc/mcpgen/internal/config" -) - -func TestToPascalCase(t *testing.T) { - tests := []struct { - input string - want string - }{ - {"hello", "Hello"}, - {"hello_world", "HelloWorld"}, - {"hello-world", "HelloWorld"}, - {"hello world", "HelloWorld"}, - {"my_api_key", "MyApiKey"}, - {"user-profile-settings", "UserProfileSettings"}, - {"_leading", "Leading"}, - {"trailing_", "Trailing"}, - {"", ""}, - } - - for _, tt := range tests { - t.Run(tt.input, func(t *testing.T) { - got := toPascalCase(tt.input) - if got != tt.want { - t.Errorf("toPascalCase(%q) = %q, want %q", tt.input, got, tt.want) - } - }) - } -} - -func TestToHandlerName(t *testing.T) { - tests := []struct { - input string - want string - }{ - {"create_task", "CreateTask"}, - {"get-user", "GetUser"}, - {"list items", "ListItems"}, - {"simple", "Simple"}, - {"my_custom_handler", "MyCustomHandler"}, - } - - for _, tt := range tests { - t.Run(tt.input, func(t *testing.T) { - got := toHandlerName(tt.input) - if got != tt.want { - t.Errorf("toHandlerName(%q) = %q, want %q", tt.input, got, tt.want) - } - }) - } -} - -func TestExtractURIParams(t *testing.T) { - tests := []struct { - name string - template string - want []string - }{ - { - name: "single parameter", - template: "users://{id}", - want: []string{"id"}, - }, - { - name: "multiple parameters", - template: "orgs://{orgId}/members/{memberId}", - want: []string{"orgId", "memberId"}, - }, - { - name: "no parameters", - template: "static://resource", - want: []string{}, - }, - { - name: "parameter at start", - template: "{userId}/profile", - want: []string{"userId"}, - }, - { - name: "parameter at end", - template: "users/{id}", - want: []string{"id"}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := extractURIParams(tt.template) - if len(got) != len(tt.want) { - t.Errorf("extractURIParams(%q) returned %d params, want %d", tt.template, len(got), len(tt.want)) - return - } - for i, param := range got { - if paramName, ok := param["Name"].(string); ok { - if paramName != tt.want[i] { - t.Errorf("extractURIParams(%q)[%d] = %q, want %q", tt.template, i, paramName, tt.want[i]) - } - } else { - t.Errorf("extractURIParams(%q)[%d] missing Name field", tt.template, i) - } - } - }) - } -} - -func TestGenerateSchemaCode(t *testing.T) { - gen := &Generator{} - - tests := []struct { - name string - schema *config.Schema - want string - }{ - { - name: "nil schema", - schema: nil, - want: "mustUnmarshalSchema(`null`)", // JSON marshals nil to "null" - }, - { - name: "simple schema", - schema: &config.Schema{ - Type: "string", - }, - want: "mustUnmarshalSchema(`{\"type\":\"string\"}`)", - }, - { - name: "schema with properties", - schema: &config.Schema{ - Type: "object", - Properties: map[string]*config.Schema{ - "name": {Type: "string"}, - }, - }, - want: "mustUnmarshalSchema(", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := gen.generateSchemaCode(tt.schema) - if !containsString(got, tt.want) { - t.Errorf("generateSchemaCode() = %q, should contain %q", got, tt.want) - } - }) - } -} - -func TestContains(t *testing.T) { - tests := []struct { - name string - slice []string - item string - want bool - }{ - { - name: "item exists", - slice: []string{"foo", "bar", "baz"}, - item: "bar", - want: true, - }, - { - name: "item does not exist", - slice: []string{"foo", "bar", "baz"}, - item: "qux", - want: false, - }, - { - name: "empty slice", - slice: []string{}, - item: "foo", - want: false, - }, - { - name: "nil slice", - slice: nil, - item: "foo", - want: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := contains(tt.slice, tt.item) - if got != tt.want { - t.Errorf("contains(%v, %q) = %v, want %v", tt.slice, tt.item, got, tt.want) - } - }) - } -} - -func TestGetRequiredHandlerNames(t *testing.T) { - spec := &config.MCPSpec{ - Tools: []config.Tool{ - {Name: "create_task"}, - {Name: "update-task"}, - }, - Resources: []config.Resource{ - {Name: "task_resource"}, - }, - Prompts: []config.Prompt{ - {Name: "help_prompt"}, - }, - } - - cfg := &config.Config{} - gen := New(cfg, spec) - - got := gen.getRequiredHandlerNames() - - if len(got) != 4 { - t.Errorf("getRequiredHandlerNames() returned %d handlers, want 4", len(got)) - } - - expected := map[string]bool{ - "CreateTaskTool": true, - "UpdateTaskTool": true, - "TaskResourceResource": true, - "HelpPromptPrompt": true, - } - - for _, name := range got { - if !expected[name] { - t.Errorf("Unexpected handler name: %q", name) - } - delete(expected, name) - } - - if len(expected) > 0 { - t.Errorf("Missing handler names: %v", expected) - } -} - -func TestLoadSchemas(t *testing.T) { - specPath := filepath.Join("testdata", "custom_types.yaml") - spec, err := config.LoadMCPSpec(specPath) - require.NoError(t, err, "Failed to load spec") - - cfg := &config.Config{ - Spec: specPath, - Output: t.TempDir(), - Model: config.ModelConfig{ - Package: "test", - Filename: "models.go", - }, - Models: config.ModelsConfig{ - Models: map[string]config.TypeMapping{}, - }, - } - - gen := New(cfg, spec) - - err = gen.loadSchemas() - if err != nil { - t.Fatalf("loadSchemas() error = %v", err) - } - - componentSchemas := []string{"Timestamp", "UUID", "Decimal", "Metadata", "Duration", "Status", "Task", "OptionalFields", "Project"} - for _, schemaName := range componentSchemas { - if _, exists := gen.typeGen.schemas[schemaName]; !exists { - t.Errorf("Expected schema %q to be loaded", schemaName) - } - } - - toolInputs := []string{"CreateTaskInput", "UpdateTaskInput", "CreateProjectInput"} - for _, inputName := range toolInputs { - if _, exists := gen.typeGen.schemas[inputName]; !exists { - t.Errorf("Expected tool input schema %q to be loaded", inputName) - } - } - - resourceSchemas := []string{"TaskResourceContent", "ProjectResourceContent"} - for _, schemaName := range resourceSchemas { - if _, exists := gen.typeGen.schemas[schemaName]; !exists { - t.Errorf("Expected resource schema %q to be loaded", schemaName) - } - } - - promptArgs := []string{"TaskSummaryArgs"} - for _, argsName := range promptArgs { - if _, exists := gen.typeGen.schemas[argsName]; !exists { - t.Errorf("Expected prompt args schema %q to be loaded", argsName) - } - } - - customMappings := []string{"Timestamp", "UUID", "Decimal", "Metadata", "Duration"} - for _, schemaName := range customMappings { - if _, exists := gen.typeGen.customMappings[schemaName]; !exists { - t.Errorf("Expected custom mapping for %q from go.probo.inc/mcpgen/type annotation", schemaName) - } - } -} - -func TestLoadSchemasWithConfigMappings(t *testing.T) { - specPath := filepath.Join("testdata", "config_based_types.yaml") - spec, err := config.LoadMCPSpec(specPath) - require.NoError(t, err, "Failed to load spec") - - cfg := &config.Config{ - Spec: specPath, - Output: t.TempDir(), - Model: config.ModelConfig{ - Package: "test", - Filename: "models.go", - }, - Models: config.ModelsConfig{ - Models: map[string]config.TypeMapping{ - "Timestamp": {Model: "time.Time"}, - "UUID": {Model: "github.com/google/uuid.UUID"}, - "User": {Model: "github.com/myapp/models.User"}, - }, - }, - } - - gen := New(cfg, spec) - - if _, exists := gen.typeGen.customMappings["Timestamp"]; !exists { - t.Error("Expected custom mapping for Timestamp from config") - } - if _, exists := gen.typeGen.customMappings["UUID"]; !exists { - t.Error("Expected custom mapping for UUID from config") - } - if _, exists := gen.typeGen.customMappings["User"]; !exists { - t.Error("Expected custom mapping for User from config") - } - - err = gen.loadSchemas() - if err != nil { - t.Fatalf("loadSchemas() error = %v", err) - } - - expectedSchemas := []string{"Timestamp", "UUID", "User", "Event"} - for _, schemaName := range expectedSchemas { - if _, exists := gen.typeGen.schemas[schemaName]; !exists { - t.Errorf("Expected schema %q to be loaded", schemaName) - } - } -} - -func TestBuildServerTemplateData(t *testing.T) { - spec := &config.MCPSpec{ - Info: config.ServerInfo{ - Title: "test-server", - Version: "1.0.0", - }, - Tools: []config.Tool{ - { - Name: "create_task", - Description: "Create a task", - InputSchema: &config.Schema{ - Type: "object", - Properties: map[string]*config.Schema{ - "title": {Type: "string"}, - }, - }, - }, - }, - Resources: []config.Resource{ - { - Name: "task", - URITemplate: "task://{id}", - Description: "Task resource", - }, - }, - Prompts: []config.Prompt{ - { - Name: "help", - Description: "Get help", - Arguments: []config.PromptArgument{ - {Name: "topic", Required: true}, - }, - }, - }, - } - - cfg := &config.Config{ - Model: config.ModelConfig{ - Package: "test", - }, - Resolver: config.ResolverConfig{ - Package: "test", - Type: "Resolver", - }, - } - - gen := New(cfg, spec) - data := gen.buildServerTemplateData() - - if data["ServerName"] != "test-server" { - t.Errorf("ServerName = %v, want test-server", data["ServerName"]) - } - if data["ServerVersion"] != "1.0.0" { - t.Errorf("ServerVersion = %v, want 1.0.0", data["ServerVersion"]) - } - - tools, ok := data["Tools"].([]map[string]interface{}) - if !ok { - t.Fatal("Tools should be []map[string]interface{}") - } - if len(tools) != 1 { - t.Errorf("Expected 1 tool, got %d", len(tools)) - } - if tools[0]["Name"] != "create_task" { - t.Errorf("Tool name = %v, want create_task", tools[0]["Name"]) - } - if tools[0]["HandlerName"] != "CreateTask" { - t.Errorf("Handler name = %v, want CreateTask", tools[0]["HandlerName"]) - } - - resources, ok := data["Resources"].([]map[string]interface{}) - if !ok { - t.Fatal("Resources should be []map[string]interface{}") - } - if len(resources) != 1 { - t.Errorf("Expected 1 resource, got %d", len(resources)) - } - if resources[0]["Name"] != "task" { - t.Errorf("Resource name = %v, want task", resources[0]["Name"]) - } - - prompts, ok := data["Prompts"].([]map[string]interface{}) - if !ok { - t.Fatal("Prompts should be []map[string]interface{}") - } - if len(prompts) != 1 { - t.Errorf("Expected 1 prompt, got %d", len(prompts)) - } - if prompts[0]["Name"] != "help" { - t.Errorf("Prompt name = %v, want help", prompts[0]["Name"]) - } - - if data["HasResources"] != true { - t.Error("HasResources should be true") - } - if data["HasPrompts"] != true { - t.Error("HasPrompts should be true") - } -} - -func TestExtractOrphanedHandlerNames(t *testing.T) { - // Create a temporary file with orphaned handlers section - content := `package test - -// Some code here - -// ============================================================================== -// Orphaned Handlers -// ============================================================================== -// The following handlers were found in the resolver file but are no longer -// defined in the MCP specification. They have been preserved here as comments -// in case you need to reference or restore them. -// ============================================================================== - -// Orphaned: OldHandler -func (r *Resolver) OldHandler() { -} - -// Orphaned: AnotherOldHandler -func (r *Resolver) AnotherOldHandler() { -} -` - - tmpFile := filepath.Join(t.TempDir(), "resolver.go") - if err := writeFile(tmpFile, []byte(content)); err != nil { - t.Fatalf("Failed to write test file: %v", err) - } - - got := extractOrphanedHandlerNames(tmpFile) - - expected := []string{"OldHandler", "AnotherOldHandler"} - if len(got) != len(expected) { - t.Errorf("extractOrphanedHandlerNames() returned %d names, want %d", len(got), len(expected)) - return - } - - for i, name := range expected { - if got[i] != name { - t.Errorf("extractOrphanedHandlerNames()[%d] = %q, want %q", i, got[i], name) - } - } -} - -func TestExtractOrphanedHandlerNamesNoSection(t *testing.T) { - // Create a file without orphaned section - content := `package test - -func (r *Resolver) NormalHandler() { -} -` - - tmpFile := filepath.Join(t.TempDir(), "resolver.go") - if err := writeFile(tmpFile, []byte(content)); err != nil { - t.Fatalf("Failed to write test file: %v", err) - } - - got := extractOrphanedHandlerNames(tmpFile) - - if len(got) != 0 { - t.Errorf("extractOrphanedHandlerNames() returned %d names, want 0", len(got)) - } -} - -func writeFile(path string, content []byte) error { - return os.WriteFile(path, content, 0644) -} - -func TestGenerateModels(t *testing.T) { - specPath := filepath.Join("testdata", "custom_types.yaml") - spec, err := config.LoadMCPSpec(specPath) - require.NoError(t, err, "Failed to load spec") - - outputDir := t.TempDir() - cfg := &config.Config{ - Spec: specPath, - Output: outputDir, - Model: config.ModelConfig{ - Package: "test", - Filename: "models.go", - }, - Resolver: config.ResolverConfig{ - Package: "test", - Type: "Resolver", - }, - Models: config.ModelsConfig{ - Models: map[string]config.TypeMapping{}, - }, - } - - gen := New(cfg, spec) - - // Load schemas first - if err := gen.loadSchemas(); err != nil { - t.Fatalf("loadSchemas() error = %v", err) - } - - if err := gen.generateModels(); err != nil { - t.Fatalf("generateModels() error = %v", err) - } - - modelsPath := filepath.Join(outputDir, "models.go") - if _, err := os.Stat(modelsPath); os.IsNotExist(err) { - t.Errorf("models.go should be created at %s", modelsPath) - } - - content, err := os.ReadFile(modelsPath) - require.NoError(t, err, "Failed to read models.go") - - codeStr := string(content) - - if !containsString(codeStr, "package test") { - t.Error("Generated models should have correct package declaration") - } - - if containsString(codeStr, "type Timestamp") { - t.Error("Should not generate Timestamp type (has go.probo.inc/mcpgen/type)") - } - if containsString(codeStr, "type UUID") { - t.Error("Should not generate UUID type (has go.probo.inc/mcpgen/type)") - } - - if !containsString(codeStr, "type Task struct") { - t.Error("Should generate Task type") - } - - if !containsString(codeStr, `"time"`) { - t.Error("Should import time package") - } - if !containsString(codeStr, `"github.com/google/uuid"`) { - t.Error("Should import uuid package") - } -} - -func TestFullGenerateWorkflow(t *testing.T) { - specPath := filepath.Join("testdata", "config_based_types.yaml") - spec, err := config.LoadMCPSpec(specPath) - require.NoError(t, err, "Failed to load spec") - - outputDir := t.TempDir() - cfg := &config.Config{ - Spec: specPath, - Output: outputDir, - Exec: config.ExecConfig{ - Package: "test", - Filename: "server.go", - }, - Model: config.ModelConfig{ - Package: "test", - Filename: "models.go", - }, - Resolver: config.ResolverConfig{ - Package: "test", - Filename: "resolver.go", - Type: "Resolver", - Preserve: false, - }, - Models: config.ModelsConfig{ - Models: map[string]config.TypeMapping{ - "Timestamp": {Model: "time.Time"}, - "UUID": {Model: "github.com/google/uuid.UUID"}, - }, - }, - } - - gen := New(cfg, spec) - - if err := gen.Generate(); err != nil { - t.Fatalf("Generate() error = %v", err) - } - - expectedFiles := []string{ - "models.go", - "server.go", - "resolver.go", - "schema.resolvers.go", - } - - for _, filename := range expectedFiles { - filePath := filepath.Join(outputDir, filename) - if _, err := os.Stat(filePath); os.IsNotExist(err) { - t.Errorf("Expected file %s should be created", filename) - } - } - - modelsContent, err := os.ReadFile(filepath.Join(outputDir, "models.go")) - require.NoError(t, err, "Failed to read models.go") - - modelsStr := string(modelsContent) - if !containsString(modelsStr, "type Event struct") { - t.Error("models.go should contain Event type") - } - - serverContent, err := os.ReadFile(filepath.Join(outputDir, "server.go")) - require.NoError(t, err, "Failed to read server.go") - - serverStr := string(serverContent) - if !containsString(serverStr, "func New(") { - t.Error("server.go should contain New function") - } - if !containsString(serverStr, "mcp.NewServer") { - t.Error("server.go should use MCP SDK") - } - - resolverContent, err := os.ReadFile(filepath.Join(outputDir, "resolver.go")) - require.NoError(t, err, "Failed to read resolver.go") - - resolverStr := string(resolverContent) - if !containsString(resolverStr, "type Resolver struct") { - t.Error("resolver.go should contain Resolver struct") - } - - resolversContent, err := os.ReadFile(filepath.Join(outputDir, "schema.resolvers.go")) - require.NoError(t, err, "Failed to read schema.resolvers.go") - - resolversStr := string(resolversContent) - if !containsString(resolversStr, "func (r *Resolver) CreateEvent") { - t.Error("schema.resolvers.go should contain CreateEvent handler") - } -} - -func TestGenerateWithDifferentPackages(t *testing.T) { - specPath := filepath.Join("testdata", "custom_types.yaml") - spec, err := config.LoadMCPSpec(specPath) - require.NoError(t, err, "Failed to load spec") - - outputDir := t.TempDir() - cfg := &config.Config{ - Spec: specPath, - Output: outputDir, - Exec: config.ExecConfig{ - Package: "server", - Filename: "server.go", - }, - Model: config.ModelConfig{ - Package: "types", - Filename: "types/models.go", - }, - Resolver: config.ResolverConfig{ - Package: "mcp_v1", - Filename: "resolver.go", - Type: "Resolver", - Preserve: false, - }, - Models: config.ModelsConfig{ - Models: map[string]config.TypeMapping{}, - }, - } - - gen := New(cfg, spec) - - // Load schemas - if err := gen.loadSchemas(); err != nil { - t.Fatalf("loadSchemas() error = %v", err) - } - - data := gen.buildServerTemplateData() - - // When model package differs from exec package, should have imports - imports, ok := data["Imports"] - assert.True(t, ok, "Should have Imports when packages differ") - - if imports != nil { - importList, ok := imports.([]map[string]string) - require.True(t, ok, "Imports should be []map[string]string") - assert.NotEmpty(t, importList, "Imports list should not be empty when packages differ") - } -} - -func TestCountOrphanedHandlers(t *testing.T) { - tests := []struct { - name string - code string - want int - }{ - { - name: "no orphaned handlers", - code: "func (r *Resolver) Handler() {}", - want: 0, - }, - { - name: "one orphaned handler", - code: "// Orphaned: OldHandler\nfunc (r *Resolver) OldHandler() {}", - want: 1, - }, - { - name: "multiple orphaned handlers", - code: "// Orphaned: Handler1\nfunc (r *Resolver) Handler1() {}\n// Orphaned: Handler2\nfunc (r *Resolver) Handler2() {}", - want: 2, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := countOrphanedHandlers(tt.code) - if got != tt.want { - t.Errorf("countOrphanedHandlers() = %d, want %d", got, tt.want) - } - }) - } -} - -func TestGenerateNewHandlersCode(t *testing.T) { - specPath := filepath.Join("testdata", "config_based_types.yaml") - spec, err := config.LoadMCPSpec(specPath) - require.NoError(t, err, "Failed to load spec") - - cfg := &config.Config{ - Spec: specPath, - Output: t.TempDir(), - Model: config.ModelConfig{ - Package: "test", - Filename: "models.go", - }, - Resolver: config.ResolverConfig{ - Package: "test", - Filename: "resolver.go", - Type: "Resolver", - Preserve: false, - }, - Models: config.ModelsConfig{ - Models: map[string]config.TypeMapping{}, - }, - } - - gen := New(cfg, spec) - if err := gen.loadSchemas(); err != nil { - t.Fatalf("Failed to load schemas: %v", err) - } - - tests := []struct { - name string - handlerNames []string - wantContains []string - isEmpty bool - }{ - { - name: "empty handler list", - handlerNames: []string{}, - isEmpty: true, - }, - { - name: "single handler", - handlerNames: []string{"CreateEventTool"}, - wantContains: []string{"CreateEventTool", "func (r *Resolver) CreateEventTool"}, - isEmpty: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - code, err := gen.generateNewHandlersCode(tt.handlerNames) - if err != nil { - t.Errorf("generateNewHandlersCode() error = %v", err) - return - } - - if tt.isEmpty { - if code != "" { - t.Errorf("Expected empty code, got: %q", code) - } - return - } - - for _, want := range tt.wantContains { - if !containsString(code, want) { - t.Errorf("Generated code should contain %q\nGot: %s", want, code) - } - } - }) - } -} - -func TestResolveAllRefs(t *testing.T) { - spec := &config.MCPSpec{ - Components: config.Components{ - Schemas: map[string]*config.Schema{ - "SimpleType": { - Type: "string", - Format: "uuid", - }, - "ComplexType": { - Type: "object", - Properties: map[string]*config.Schema{ - "id": { - Ref: "#/components/schemas/SimpleType", - }, - "name": { - Type: "string", - }, - }, - }, - "WithExtensions": { - Type: "string", - Format: "date-time", - Extra: map[string]interface{}{ - "go.probo.inc/mcpgen/type": "time.Time", - "x-custom-field": "value", - }, - }, - "NestedRef": { - Type: "object", - Properties: map[string]*config.Schema{ - "complex": { - Ref: "#/components/schemas/ComplexType", - }, - }, - }, - }, - }, - } - - cfg := &config.Config{} - gen := New(cfg, spec) - - tests := []struct { - name string - schema *config.Schema - wantType string - wantNoRef bool - wantNoExtras bool - checkProperty string - propertyType string - }{ - { - name: "nil schema", - schema: nil, - wantType: "", - wantNoRef: true, - }, - { - name: "simple ref resolution", - schema: &config.Schema{ - Ref: "#/components/schemas/SimpleType", - }, - wantType: "string", - wantNoRef: true, - }, - { - name: "nested ref in properties", - schema: &config.Schema{ - Ref: "#/components/schemas/ComplexType", - }, - wantType: "object", - wantNoRef: true, - checkProperty: "id", - propertyType: "string", - }, - { - name: "deep nested refs", - schema: &config.Schema{ - Ref: "#/components/schemas/NestedRef", - }, - wantType: "object", - wantNoRef: true, - checkProperty: "complex", - propertyType: "object", - }, - { - name: "ref in array items", - schema: &config.Schema{ - Type: "array", - Items: &config.Schema{ - Ref: "#/components/schemas/SimpleType", - }, - }, - wantType: "array", - wantNoRef: true, - }, - { - name: "ref in anyOf", - schema: &config.Schema{ - AnyOf: []*config.Schema{ - { - Ref: "#/components/schemas/SimpleType", - }, - { - Type: "null", - }, - }, - }, - wantNoRef: true, - }, - { - name: "removes x-* extensions", - schema: &config.Schema{ - Ref: "#/components/schemas/WithExtensions", - }, - wantType: "string", - wantNoRef: true, - wantNoExtras: true, - }, - { - name: "no ref to resolve", - schema: &config.Schema{ - Type: "string", - }, - wantType: "string", - wantNoRef: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := gen.resolveAllRefs(tt.schema) - if err != nil { - t.Fatalf("resolveAllRefs() error = %v", err) - } - - if tt.schema == nil { - if got != nil { - t.Errorf("resolveAllRefs(nil) should return nil") - } - return - } - - if tt.wantType != "" && got.Type != tt.wantType { - t.Errorf("Type = %q, want %q", got.Type, tt.wantType) - } - - if tt.wantNoRef && got.Ref != "" { - t.Errorf("Ref should be empty, got %q", got.Ref) - } - - if tt.wantNoExtras && got.Extra != nil && len(got.Extra) > 0 { - t.Errorf("Extra fields should be removed, got %v", got.Extra) - } - - if tt.checkProperty != "" { - if got.Properties == nil { - t.Error("Properties should not be nil") - return - } - prop, ok := got.Properties[tt.checkProperty] - if !ok { - t.Errorf("Property %q should exist", tt.checkProperty) - return - } - if prop.Type != tt.propertyType { - t.Errorf("Property %q type = %q, want %q", tt.checkProperty, prop.Type, tt.propertyType) - } - if prop.Ref != "" { - t.Errorf("Property %q should have ref resolved, got %q", tt.checkProperty, prop.Ref) - } - } - - if tt.schema.Type == "array" && got.Items != nil { - if got.Items.Ref != "" { - t.Errorf("Array items should have ref resolved, got %q", got.Items.Ref) - } - } - - if len(tt.schema.AnyOf) > 0 { - for i, schema := range got.AnyOf { - if schema.Ref != "" { - t.Errorf("anyOf[%d] should have ref resolved, got %q", i, schema.Ref) - } - } - } - }) - } -} - -func TestResolveAllRefsInAllOf(t *testing.T) { - spec := &config.MCPSpec{ - Components: config.Components{ - Schemas: map[string]*config.Schema{ - "Base": { - Type: "object", - Properties: map[string]*config.Schema{ - "id": {Type: "string"}, - }, - }, - "Extended": { - Type: "object", - Properties: map[string]*config.Schema{ - "name": {Type: "string"}, - }, - }, - }, - }, - } - - cfg := &config.Config{} - gen := New(cfg, spec) - - schema := &config.Schema{ - AllOf: []*config.Schema{ - {Ref: "#/components/schemas/Base"}, - {Ref: "#/components/schemas/Extended"}, - }, - } - - got, err := gen.resolveAllRefs(schema) - if err != nil { - t.Fatalf("resolveAllRefs() error = %v", err) - } - - if len(got.AllOf) != 2 { - t.Errorf("AllOf length = %d, want 2", len(got.AllOf)) - } - - for i, s := range got.AllOf { - if s.Ref != "" { - t.Errorf("allOf[%d] should have ref resolved, got %q", i, s.Ref) - } - if s.Type != "object" { - t.Errorf("allOf[%d] type = %q, want object", i, s.Type) - } - } -} - -func TestResolveAllRefsInOneOf(t *testing.T) { - spec := &config.MCPSpec{ - Components: config.Components{ - Schemas: map[string]*config.Schema{ - "Option1": {Type: "string"}, - "Option2": {Type: "number"}, - }, - }, - } - - cfg := &config.Config{} - gen := New(cfg, spec) - - schema := &config.Schema{ - OneOf: []*config.Schema{ - {Ref: "#/components/schemas/Option1"}, - {Ref: "#/components/schemas/Option2"}, - }, - } - - got, err := gen.resolveAllRefs(schema) - if err != nil { - t.Fatalf("resolveAllRefs() error = %v", err) - } - - if len(got.OneOf) != 2 { - t.Errorf("OneOf length = %d, want 2", len(got.OneOf)) - } - - for i, s := range got.OneOf { - if s.Ref != "" { - t.Errorf("oneOf[%d] should have ref resolved, got %q", i, s.Ref) - } - } -} - -func TestResolveAllRefsInAdditionalProperties(t *testing.T) { - spec := &config.MCPSpec{ - Components: config.Components{ - Schemas: map[string]*config.Schema{ - "Value": {Type: "string"}, - }, - }, - } - - cfg := &config.Config{} - gen := New(cfg, spec) - - schema := &config.Schema{ - Type: "object", - AdditionalProperties: &config.Schema{ - Ref: "#/components/schemas/Value", - }, - } - - got, err := gen.resolveAllRefs(schema) - if err != nil { - t.Fatalf("resolveAllRefs() error = %v", err) - } - - if got.AdditionalProperties == nil { - t.Fatal("AdditionalProperties should not be nil") - } - - if got.AdditionalProperties.Ref != "" { - t.Errorf("AdditionalProperties should have ref resolved, got %q", got.AdditionalProperties.Ref) - } - - if got.AdditionalProperties.Type != "string" { - t.Errorf("AdditionalProperties type = %q, want string", got.AdditionalProperties.Type) - } -} - -func TestResolveAllRefsInPatternProperties(t *testing.T) { - spec := &config.MCPSpec{ - Components: config.Components{ - Schemas: map[string]*config.Schema{ - "Pattern": {Type: "number"}, - }, - }, - } - - cfg := &config.Config{} - gen := New(cfg, spec) - - schema := &config.Schema{ - Type: "object", - PatternProperties: map[string]*config.Schema{ - "^[a-z]+$": { - Ref: "#/components/schemas/Pattern", - }, - }, - } - - got, err := gen.resolveAllRefs(schema) - if err != nil { - t.Fatalf("resolveAllRefs() error = %v", err) - } - - if got.PatternProperties == nil { - t.Fatal("PatternProperties should not be nil") - } - - pattern, ok := got.PatternProperties["^[a-z]+$"] - if !ok { - t.Fatal("Pattern should exist") - } - - if pattern.Ref != "" { - t.Errorf("Pattern should have ref resolved, got %q", pattern.Ref) - } - - if pattern.Type != "number" { - t.Errorf("Pattern type = %q, want number", pattern.Type) - } -} - -func TestResolveAllRefsError(t *testing.T) { - spec := &config.MCPSpec{ - Components: config.Components{ - Schemas: map[string]*config.Schema{}, - }, - } - - cfg := &config.Config{} - gen := New(cfg, spec) - - schema := &config.Schema{ - Ref: "#/components/schemas/NonExistent", - } - - _, err := gen.resolveAllRefs(schema) - if err == nil { - t.Error("resolveAllRefs() should return error for non-existent ref") - } -} - -func TestResolveAllRefsRemovesExtensions(t *testing.T) { - spec := &config.MCPSpec{ - Components: config.Components{ - Schemas: map[string]*config.Schema{ - "TypeWithExtensions": { - Type: "object", - Extra: map[string]interface{}{ - "go.probo.inc/mcpgen/type": "custom.Type", - "x-custom": "value", - }, - Properties: map[string]*config.Schema{ - "field": { - Type: "string", - Extra: map[string]interface{}{ - "x-validation": "required", - }, - }, - }, - }, - }, - }, - } - - cfg := &config.Config{} - gen := New(cfg, spec) - - schema := &config.Schema{ - Ref: "#/components/schemas/TypeWithExtensions", - } - - got, err := gen.resolveAllRefs(schema) - if err != nil { - t.Fatalf("resolveAllRefs() error = %v", err) - } - - if got.Extra != nil && len(got.Extra) > 0 { - t.Errorf("Extra should be removed from resolved schema, got %v", got.Extra) - } - - if got.Properties == nil { - t.Fatal("Properties should not be nil") - } - - field := got.Properties["field"] - if field.Extra != nil && len(field.Extra) > 0 { - t.Errorf("Extra should be removed from property, got %v", field.Extra) - } -} - -func TestUpdateResolverIncremental(t *testing.T) { - specPath := filepath.Join("testdata", "config_based_types.yaml") - spec, err := config.LoadMCPSpec(specPath) - require.NoError(t, err, "Failed to load spec") - - tests := []struct { - name string - existingResolver string - wantContains []string - wantNotContains []string - expectUpdate bool - }{ - { - name: "add new handler", - existingResolver: `package test - -import ( - "context" - "fmt" - mcp "github.com/mark3labs/mcp-go/mcp" -) - -type Resolver struct{} - -type toolResolver struct { - *Resolver -} - -type promptResolver struct { - *Resolver -} - -type resourceResolver struct { - *Resolver -} - -func (r *toolResolver) OldHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, map[string]any, error) { - return nil, nil, nil -} -`, - wantContains: []string{ - "func (r *Resolver) CreateEventTool", - "Orphaned: OldHandler", - }, - expectUpdate: true, - }, - { - name: "no changes needed", - existingResolver: `package test - -import ( - "context" - "fmt" - mcp "github.com/mark3labs/mcp-go/mcp" -) - -type Resolver struct{} - -type toolResolver struct { - *Resolver -} - -type promptResolver struct { - *Resolver -} - -type resourceResolver struct { - *Resolver -} - -func (r *Resolver) CreateEventTool(ctx context.Context, req *mcp.CallToolRequest, input *Event) (*mcp.CallToolResult, map[string]any, error) { - return nil, nil, fmt.Errorf("create-event not implemented") -} -`, - wantContains: []string{ - "func (r *Resolver) CreateEventTool", - }, - wantNotContains: []string{ - "Orphaned Handlers", - }, - expectUpdate: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tmpDir := t.TempDir() - resolverFile := filepath.Join(tmpDir, "schema.resolvers.go") - - if err := os.WriteFile(resolverFile, []byte(tt.existingResolver), 0644); err != nil { - t.Fatalf("Failed to write resolver file: %v", err) - } - - cfg := &config.Config{ - Spec: specPath, - Output: tmpDir, - Model: config.ModelConfig{ - Package: "test", - Filename: "models.go", - }, - Resolver: config.ResolverConfig{ - Package: "test", - Filename: "schema.resolvers.go", - Type: "Resolver", - Preserve: true, - }, - Models: config.ModelsConfig{ - Models: map[string]config.TypeMapping{}, - }, - } - - gen := New(cfg, spec) - if err := gen.loadSchemas(); err != nil { - t.Fatalf("Failed to load schemas: %v", err) - } - - err := gen.updateResolverIncremental(resolverFile) - if err != nil { - t.Fatalf("updateResolverIncremental() error = %v", err) - } - - content, err := os.ReadFile(resolverFile) - if err != nil { - t.Fatalf("Failed to read updated resolver: %v", err) - } - - contentStr := string(content) - - for _, want := range tt.wantContains { - if !containsString(contentStr, want) { - t.Errorf("Updated resolver should contain %q\nGot: %s", want, contentStr) - } - } - - for _, wantNot := range tt.wantNotContains { - if containsString(contentStr, wantNot) { - t.Errorf("Updated resolver should NOT contain %q", wantNot) - } - } - }) - } -} - diff --git a/third_party/mcpgen/internal/codegen/integration_test.go b/third_party/mcpgen/internal/codegen/integration_test.go deleted file mode 100644 index f7a261ba5..000000000 --- a/third_party/mcpgen/internal/codegen/integration_test.go +++ /dev/null @@ -1,282 +0,0 @@ -package codegen - -import ( - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "go.probo.inc/mcpgen/internal/config" -) - -func TestGenerateWithCustomTypes(t *testing.T) { - specPath := filepath.Join("testdata", "custom_types.yaml") - spec, err := config.LoadMCPSpec(specPath) - require.NoError(t, err, "Failed to load spec") - - cfg := &config.Config{ - Spec: specPath, - Output: t.TempDir(), - Model: config.ModelConfig{ - Package: "test", - Filename: "models.go", - }, - Resolver: config.ResolverConfig{ - Package: "test", - Filename: "resolver.go", - Type: "Resolver", - Preserve: false, - }, - // No custom models in config - using go.probo.inc/mcpgen/type annotations - Models: config.ModelsConfig{ - Models: map[string]config.TypeMapping{}, - }, - } - - gen := New(cfg, spec) - - if err := gen.loadSchemas(); err != nil { - t.Fatalf("Failed to load schemas: %v", err) - } - - code, err := gen.typeGen.Generate("test") - require.NoError(t, err, "Failed to generate code") - - codeStr := string(code) - - customTypes := []string{"Timestamp", "UUID", "Decimal", "Metadata", "Duration"} - for _, typeName := range customTypes { - assert.NotContains(t, codeStr, "type "+typeName+" ") - } - - regularTypes := []string{"Task", "OptionalFields", "Project", "UpdateTaskInput"} - for _, typeName := range regularTypes { - assert.Contains(t, codeStr, "type "+typeName) - } - - expectedImports := []string{ - "time", - "github.com/google/uuid", - "github.com/shopspring/decimal", - "json", - "go.probo.inc/mcpgen/mcp", - } - for _, imp := range expectedImports { - assert.Contains(t, codeStr, `"`+imp+`"`) - } - - assert.Contains(t, codeStr, "ID uuid.UUID") - assert.Contains(t, codeStr, "CreatedAt time.Time") - assert.Contains(t, codeStr, "UpdatedAt *time.Time") - assert.Contains(t, codeStr, "mcp.Omittable[*string]") - assert.Contains(t, codeStr, "mcp.Omittable[*Status]") - assert.Contains(t, codeStr, "mcp.Omittable[*int]") - assert.Contains(t, codeStr, "mcp.Omittable[*[]string]") -} - -func TestGenerateWithConfigBasedTypes(t *testing.T) { - specPath := filepath.Join("testdata", "config_based_types.yaml") - spec, err := config.LoadMCPSpec(specPath) - require.NoError(t, err, "Failed to load spec") - - cfg := &config.Config{ - Spec: specPath, - Output: t.TempDir(), - Model: config.ModelConfig{ - Package: "test", - Filename: "models.go", - }, - Resolver: config.ResolverConfig{ - Package: "test", - Filename: "resolver.go", - Type: "Resolver", - Preserve: false, - }, - // Custom models in config - Models: config.ModelsConfig{ - Models: map[string]config.TypeMapping{ - "Timestamp": {Model: "time.Time"}, - "UUID": {Model: "github.com/google/uuid.UUID"}, - "User": {Model: "github.com/myapp/models.User"}, - }, - }, - } - - gen := New(cfg, spec) - - if err := gen.loadSchemas(); err != nil { - t.Fatalf("Failed to load schemas: %v", err) - } - - code, err := gen.typeGen.Generate("test") - require.NoError(t, err, "Failed to generate code") - - codeStr := string(code) - - customTypes := []string{"Timestamp", "UUID", "User"} - for _, typeName := range customTypes { - assert.NotContains(t, codeStr, "type "+typeName+" ") - } - - assert.Contains(t, codeStr, "type Event struct") - - expectedImports := []string{ - "time", - "github.com/google/uuid", - "github.com/myapp/models", - } - for _, imp := range expectedImports { - assert.Contains(t, codeStr, `"`+imp+`"`) - } - - assert.Contains(t, codeStr, "ID uuid.UUID") - assert.Contains(t, codeStr, "CreatedAt time.Time") - assert.Contains(t, codeStr, "Owner *models.User") -} - -func TestGenerateAllPrimitives(t *testing.T) { - specPath := filepath.Join("testdata", "all_primitives.yaml") - spec, err := config.LoadMCPSpec(specPath) - require.NoError(t, err, "Failed to load spec") - - cfg := &config.Config{ - Spec: specPath, - Output: t.TempDir(), - Model: config.ModelConfig{ - Package: "test", - Filename: "models.go", - }, - Resolver: config.ResolverConfig{ - Package: "test", - Filename: "resolver.go", - Type: "Resolver", - Preserve: false, - }, - Models: config.ModelsConfig{ - Models: map[string]config.TypeMapping{}, - }, - } - - gen := New(cfg, spec) - - if err := gen.loadSchemas(); err != nil { - t.Fatalf("Failed to load schemas: %v", err) - } - - code, err := gen.typeGen.Generate("test") - require.NoError(t, err, "Failed to generate code") - - codeStr := string(code) - - primitiveTypes := map[string]string{ - "StringSchema": "type StringSchema string", - "NumberSchema": "type NumberSchema float64", - "IntegerSchema": "type IntegerSchema int", - "BooleanSchema": "type BooleanSchema bool", - "ArraySchema": "type ArraySchema []string", - } - - for typeName, expectedDecl := range primitiveTypes { - if !containsString(codeStr, expectedDecl) { - t.Errorf("Should generate %q for %s", expectedDecl, typeName) - } - } - - if !containsString(codeStr, "type ObjectSchema struct") { - t.Error("Should generate ObjectSchema as a struct") - } - - if !containsString(codeStr, "type Person struct") { - t.Error("Should generate Person as a struct") - } - - if !containsString(codeStr, "type Color string") { - t.Error("Should generate Color as string-based enum") - } - - enumConstants := []string{"ColorRed", "ColorGreen", "ColorBlue", "ColorYellow"} - for _, constName := range enumConstants { - if !containsString(codeStr, constName) { - t.Errorf("Should generate enum constant %q", constName) - } - } - - if len(code) == 0 { - t.Error("Generated code is empty") - } -} - -func TestGeneratedCodeCompiles(t *testing.T) { - testCases := []struct { - name string - specFile string - config *config.Config - }{ - { - name: "custom_types", - specFile: "custom_types.yaml", - config: &config.Config{ - Model: config.ModelConfig{ - Package: "test", - Filename: "models.go", - }, - Models: config.ModelsConfig{ - Models: map[string]config.TypeMapping{}, - }, - }, - }, - { - name: "config_based_types", - specFile: "config_based_types.yaml", - config: &config.Config{ - Model: config.ModelConfig{ - Package: "test", - Filename: "models.go", - }, - Models: config.ModelsConfig{ - Models: map[string]config.TypeMapping{ - "Timestamp": {Model: "time.Time"}, - "UUID": {Model: "github.com/google/uuid.UUID"}, - "User": {Model: "github.com/myapp/models.User"}, - }, - }, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - specPath := filepath.Join("testdata", tc.specFile) - spec, err := config.LoadMCPSpec(specPath) - if err != nil { - t.Fatalf("Failed to load spec: %v", err) - } - - tc.config.Spec = specPath - tc.config.Output = t.TempDir() - tc.config.Resolver = config.ResolverConfig{ - Package: "test", - Filename: "resolver.go", - Type: "Resolver", - Preserve: false, - } - - gen := New(tc.config, spec) - if err := gen.loadSchemas(); err != nil { - t.Fatalf("Failed to load schemas: %v", err) - } - - code, err := gen.typeGen.Generate("test") - if err != nil { - t.Fatalf("Failed to generate code: %v", err) - } - - // The fact that Generate() succeeded means the code was formatted successfully - if len(code) == 0 { - t.Error("Generated code is empty") - } - }) - } -} - diff --git a/third_party/mcpgen/internal/codegen/parser.go b/third_party/mcpgen/internal/codegen/parser.go deleted file mode 100644 index e0c32cef3..000000000 --- a/third_party/mcpgen/internal/codegen/parser.go +++ /dev/null @@ -1,191 +0,0 @@ -package codegen - -import ( - "fmt" - "go/ast" - "go/parser" - "go/printer" - "go/token" - "os" - "strings" -) - -type HandlerInfo struct { - Name string - RecvType string - SourceCode string - IsOrphaned bool -} - -type ResolverParser struct { - filePath string - fset *token.FileSet - file *ast.File -} - -func NewResolverParser(filePath string) (*ResolverParser, error) { - fset := token.NewFileSet() - - if _, err := os.Stat(filePath); os.IsNotExist(err) { - return nil, fmt.Errorf("resolver file not found: %s", filePath) - } - - file, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments) - if err != nil { - return nil, fmt.Errorf("failed to parse resolver file: %w", err) - } - - return &ResolverParser{ - filePath: filePath, - fset: fset, - file: file, - }, nil -} - -func (p *ResolverParser) ExtractHandlers(resolverType string) (map[string]*HandlerInfo, error) { - handlers := make(map[string]*HandlerInfo) - - // Extract from both old wrapper types and new direct Resolver type - allowedTypes := map[string]bool{ - // Old wrapper types (for backward compatibility during migration) - "toolResolver": true, - "*toolResolver": true, - "promptResolver": true, - "*promptResolver": true, - "resourceResolver": true, - "*resourceResolver": true, - // New direct Resolver type - resolverType: true, - "*" + resolverType: true, - } - - for _, decl := range p.file.Decls { - funcDecl, ok := decl.(*ast.FuncDecl) - if !ok { - continue - } - - if funcDecl.Recv == nil { - continue - } - - recvType := p.getReceiverType(funcDecl.Recv) - if !allowedTypes[recvType] { - continue - } - - methodName := funcDecl.Name.Name - sourceCode, err := p.extractFunctionSource(funcDecl) - if err != nil { - return nil, fmt.Errorf("failed to extract source for %s: %w", methodName, err) - } - - // Transform receiver type from old wrapper types to main Resolver type - sourceCode = TransformReceiverType(sourceCode, resolverType) - - handlers[methodName] = &HandlerInfo{ - Name: methodName, - RecvType: "*" + resolverType, // Always use main Resolver type - SourceCode: sourceCode, - IsOrphaned: false, - } - } - - return handlers, nil -} - -func (p *ResolverParser) getReceiverType(recv *ast.FieldList) string { - if recv == nil || len(recv.List) == 0 { - return "" - } - - field := recv.List[0] - switch typ := field.Type.(type) { - case *ast.Ident: - return typ.Name - case *ast.StarExpr: - if ident, ok := typ.X.(*ast.Ident); ok { - return "*" + ident.Name - } - } - - return "" -} - -func (p *ResolverParser) extractFunctionSource(funcDecl *ast.FuncDecl) (string, error) { - var buf strings.Builder - - cfg := printer.Config{ - Mode: printer.UseSpaces | printer.TabIndent, - Tabwidth: 8, - } - - if err := cfg.Fprint(&buf, p.fset, funcDecl); err != nil { - return "", err - } - - return buf.String(), nil -} - -// TransformReceiverType rewrites the receiver type in handler source code from old wrapper types -// (toolResolver, promptResolver, resourceResolver) to the main Resolver type -func TransformReceiverType(sourceCode, resolverType string) string { - // Replace old wrapper types with main Resolver type - sourceCode = strings.ReplaceAll(sourceCode, "*toolResolver)", "*"+resolverType+")") - sourceCode = strings.ReplaceAll(sourceCode, "*promptResolver)", "*"+resolverType+")") - sourceCode = strings.ReplaceAll(sourceCode, "*resourceResolver)", "*"+resolverType+")") - return sourceCode -} - -func IdentifyOrphanedHandlers(existingHandlers map[string]*HandlerInfo, requiredHandlers []string) { - requiredSet := make(map[string]bool) - for _, name := range requiredHandlers { - requiredSet[name] = true - } - - for name, handler := range existingHandlers { - if !requiredSet[name] { - handler.IsOrphaned = true - } - } -} - -func FormatOrphanedHandlers(handlers map[string]*HandlerInfo) string { - var orphaned []*HandlerInfo - - for _, handler := range handlers { - if handler.IsOrphaned { - orphaned = append(orphaned, handler) - } - } - - if len(orphaned) == 0 { - return "" - } - - var buf strings.Builder - buf.WriteString("\n\n// ==============================================================================\n") - buf.WriteString("// Orphaned Handlers\n") - buf.WriteString("// ==============================================================================\n") - buf.WriteString("// The following handlers were found in the resolver file but are no longer\n") - buf.WriteString("// defined in the MCP specification. They have been preserved here as comments\n") - buf.WriteString("// in case you need to reference or restore them.\n") - buf.WriteString("// ==============================================================================\n\n") - - for _, handler := range orphaned { - buf.WriteString(fmt.Sprintf("// Orphaned: %s\n", handler.Name)) - buf.WriteString("// Uncomment and update signature if you want to restore this handler.\n") - - lines := strings.Split(handler.SourceCode, "\n") - for _, line := range lines { - if strings.TrimSpace(line) != "" { - buf.WriteString("// ") - } - buf.WriteString(line) - buf.WriteString("\n") - } - buf.WriteString("\n") - } - - return buf.String() -} diff --git a/third_party/mcpgen/internal/codegen/parser_test.go b/third_party/mcpgen/internal/codegen/parser_test.go deleted file mode 100644 index 96a3698c0..000000000 --- a/third_party/mcpgen/internal/codegen/parser_test.go +++ /dev/null @@ -1,484 +0,0 @@ -package codegen - -import ( - "os" - "path/filepath" - "strings" - "testing" - - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/assert" -) - -func TestNewResolverParser(t *testing.T) { - tests := []struct { - name string - content string - wantErr bool - setupFile bool - }{ - { - name: "valid resolver file", - content: `package test - -type Resolver struct{} - -func (r *Resolver) GetUser(ctx context.Context) error { - return nil -}`, - setupFile: true, - wantErr: false, - }, - { - name: "non-existent file", - content: "", - setupFile: false, - wantErr: true, - }, - { - name: "invalid Go syntax", - content: `package test -func ( { // invalid syntax -}`, - setupFile: true, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var testFile string - if tt.setupFile { - tmpDir := t.TempDir() - testFile = filepath.Join(tmpDir, "resolver.go") - if err := os.WriteFile(testFile, []byte(tt.content), 0644); err != nil { - t.Fatalf("Failed to write test file: %v", err) - } - } else { - testFile = filepath.Join(t.TempDir(), "nonexistent.go") - } - - parser, err := NewResolverParser(testFile) - - if tt.wantErr { - if err == nil { - t.Error("Expected error but got nil") - } - } else { - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - if parser == nil { - t.Error("Expected parser but got nil") - } - } - }) - } -} - -func TestExtractHandlers(t *testing.T) { - tests := []struct { - name string - content string - resolverType string - wantHandlers []string - wantErr bool - }{ - { - name: "extract toolResolver handlers", - content: `package test - -type toolResolver struct{} - -func (r *toolResolver) HandleListTasks(ctx context.Context) error { - return nil -} - -func (r *toolResolver) HandleCreateTask(ctx context.Context) error { - return nil -} - -// Not a handler - no receiver -func HelperFunction() {} -`, - resolverType: "Resolver", - wantHandlers: []string{"HandleListTasks", "HandleCreateTask"}, - wantErr: false, - }, - { - name: "extract promptResolver handlers", - content: `package test - -type promptResolver struct{} - -func (r *promptResolver) HandleGetPrompt(ctx context.Context) error { - return nil -} -`, - resolverType: "Resolver", - wantHandlers: []string{"HandleGetPrompt"}, - wantErr: false, - }, - { - name: "extract resourceResolver handlers", - content: `package test - -type resourceResolver struct{} - -func (r *resourceResolver) HandleReadResource(ctx context.Context) error { - return nil -} -`, - resolverType: "Resolver", - wantHandlers: []string{"HandleReadResource"}, - wantErr: false, - }, - { - name: "mixed resolver types", - content: `package test - -type toolResolver struct{} -type promptResolver struct{} -type resourceResolver struct{} - -func (r *toolResolver) HandleTool(ctx context.Context) error { - return nil -} - -func (r *promptResolver) HandlePrompt(ctx context.Context) error { - return nil -} - -func (r *resourceResolver) HandleResource(ctx context.Context) error { - return nil -} - -type OtherType struct{} - -func (r *OtherType) NotAHandler(ctx context.Context) error { - return nil -} -`, - resolverType: "Resolver", - wantHandlers: []string{"HandleTool", "HandlePrompt", "HandleResource"}, - wantErr: false, - }, - { - name: "no handlers", - content: `package test - -type Resolver struct{} - -func HelperFunction() {} -`, - resolverType: "Resolver", - wantHandlers: []string{}, - wantErr: false, - }, - { - name: "pointer and value receivers", - content: `package test - -type toolResolver struct{} - -func (r toolResolver) HandleNonPointer(ctx context.Context) error { - return nil -} - -func (r *toolResolver) HandlePointer(ctx context.Context) error { - return nil -} -`, - resolverType: "Resolver", - wantHandlers: []string{"HandleNonPointer", "HandlePointer"}, // Both are accepted - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tmpDir := t.TempDir() - testFile := filepath.Join(tmpDir, "resolver.go") - if err := os.WriteFile(testFile, []byte(tt.content), 0644); err != nil { - t.Fatalf("Failed to write test file: %v", err) - } - - parser, err := NewResolverParser(testFile) - if err != nil { - t.Fatalf("Failed to create parser: %v", err) - } - - handlers, err := parser.ExtractHandlers(tt.resolverType) - - if tt.wantErr { - if err == nil { - t.Error("Expected error but got nil") - } - return - } - - if err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - - if len(handlers) != len(tt.wantHandlers) { - t.Errorf("Expected %d handlers, got %d", len(tt.wantHandlers), len(handlers)) - } - - for _, wantName := range tt.wantHandlers { - if _, ok := handlers[wantName]; !ok { - t.Errorf("Expected handler %q not found", wantName) - } - } - - for name, handler := range handlers { - if handler.Name != name { - t.Errorf("Handler name mismatch: got %q, want %q", handler.Name, name) - } - if handler.SourceCode == "" { - t.Errorf("Handler %q has empty source code", name) - } - if !strings.Contains(handler.RecvType, "Resolver") { - t.Errorf("Handler %q has unexpected receiver type: %q", name, handler.RecvType) - } - } - }) - } -} - -func TestIdentifyOrphanedHandlers(t *testing.T) { - tests := []struct { - name string - existingHandlers map[string]*HandlerInfo - requiredHandlers []string - wantOrphaned []string - }{ - { - name: "no orphaned handlers", - existingHandlers: map[string]*HandlerInfo{ - "HandleA": {Name: "HandleA"}, - "HandleB": {Name: "HandleB"}, - }, - requiredHandlers: []string{"HandleA", "HandleB"}, - wantOrphaned: []string{}, - }, - { - name: "one orphaned handler", - existingHandlers: map[string]*HandlerInfo{ - "HandleA": {Name: "HandleA"}, - "HandleB": {Name: "HandleB"}, - "HandleC": {Name: "HandleC"}, - }, - requiredHandlers: []string{"HandleA", "HandleB"}, - wantOrphaned: []string{"HandleC"}, - }, - { - name: "all orphaned", - existingHandlers: map[string]*HandlerInfo{ - "HandleA": {Name: "HandleA"}, - "HandleB": {Name: "HandleB"}, - }, - requiredHandlers: []string{}, - wantOrphaned: []string{"HandleA", "HandleB"}, - }, - { - name: "new handlers required", - existingHandlers: map[string]*HandlerInfo{ - "HandleA": {Name: "HandleA"}, - }, - requiredHandlers: []string{"HandleA", "HandleB", "HandleC"}, - wantOrphaned: []string{}, - }, - { - name: "empty existing handlers", - existingHandlers: map[string]*HandlerInfo{}, - requiredHandlers: []string{"HandleA"}, - wantOrphaned: []string{}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - IdentifyOrphanedHandlers(tt.existingHandlers, tt.requiredHandlers) - - var gotOrphaned []string - for name, handler := range tt.existingHandlers { - if handler.IsOrphaned { - gotOrphaned = append(gotOrphaned, name) - } - } - - if len(gotOrphaned) != len(tt.wantOrphaned) { - t.Errorf("Expected %d orphaned handlers, got %d", len(tt.wantOrphaned), len(gotOrphaned)) - } - - orphanedSet := make(map[string]bool) - for _, name := range gotOrphaned { - orphanedSet[name] = true - } - - for _, wantName := range tt.wantOrphaned { - if !orphanedSet[wantName] { - t.Errorf("Expected %q to be orphaned but it wasn't", wantName) - } - } - }) - } -} - -func TestFormatOrphanedHandlers(t *testing.T) { - tests := []struct { - name string - handlers map[string]*HandlerInfo - wantContains []string - isEmpty bool - }{ - { - name: "single orphaned handler", - handlers: map[string]*HandlerInfo{ - "HandleOldTask": { - Name: "HandleOldTask", - IsOrphaned: true, - SourceCode: `func (r *Resolver) HandleOldTask(ctx context.Context) error { - return nil -}`, - }, - }, - wantContains: []string{ - "Orphaned Handlers", - "Orphaned: HandleOldTask", - "// func (r *Resolver) HandleOldTask", - }, - isEmpty: false, - }, - { - name: "multiple orphaned handlers", - handlers: map[string]*HandlerInfo{ - "HandleA": { - Name: "HandleA", - IsOrphaned: true, - SourceCode: "func (r *Resolver) HandleA() {}", - }, - "HandleB": { - Name: "HandleB", - IsOrphaned: true, - SourceCode: "func (r *Resolver) HandleB() {}", - }, - }, - wantContains: []string{ - "Orphaned: HandleA", - "Orphaned: HandleB", - }, - isEmpty: false, - }, - { - name: "no orphaned handlers", - handlers: map[string]*HandlerInfo{ - "HandleActive": { - Name: "HandleActive", - IsOrphaned: false, - SourceCode: "func (r *Resolver) HandleActive() {}", - }, - }, - wantContains: []string{}, - isEmpty: true, - }, - { - name: "empty handlers map", - handlers: map[string]*HandlerInfo{}, - wantContains: []string{}, - isEmpty: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := FormatOrphanedHandlers(tt.handlers) - - if tt.isEmpty { - if result != "" { - t.Errorf("Expected empty result, got: %q", result) - } - return - } - - if result == "" { - t.Error("Expected non-empty result but got empty string") - return - } - - for _, want := range tt.wantContains { - if !strings.Contains(result, want) { - t.Errorf("Result should contain %q but doesn't.\nGot: %s", want, result) - } - } - - assert.Contains(t, result, "Orphaned Handlers", "Result should contain orphaned handlers header") - }) - } -} - -func TestGetReceiverType(t *testing.T) { - // but we can add a direct test using a sample AST if needed - tmpDir := t.TempDir() - testFile := filepath.Join(tmpDir, "test.go") - - content := `package test - -type toolResolver struct{} - -func (r *toolResolver) Method() {} -` - if err := os.WriteFile(testFile, []byte(content), 0644); err != nil { - t.Fatalf("Failed to write test file: %v", err) - } - - parser, err := NewResolverParser(testFile) - require.NoError(t, err, "Failed to create parser") - - handlers, err := parser.ExtractHandlers("Resolver") - require.NoError(t, err, "Failed to extract handlers") - - if len(handlers) != 1 { - t.Fatalf("Expected 1 handler, got %d", len(handlers)) - } - - handler := handlers["Method"] - // After transformation, the receiver type should be *Resolver - if handler.RecvType != "*Resolver" { - t.Errorf("Expected receiver type '*Resolver' (after transformation), got %q", handler.RecvType) - } -} - -func TestExtractFunctionSource(t *testing.T) { - tmpDir := t.TempDir() - testFile := filepath.Join(tmpDir, "test.go") - - content := `package test - -type toolResolver struct{} - -func (r *toolResolver) HandleTest(ctx context.Context) error { - x := 42 - return nil -} -` - if err := os.WriteFile(testFile, []byte(content), 0644); err != nil { - t.Fatalf("Failed to write test file: %v", err) - } - - parser, err := NewResolverParser(testFile) - require.NoError(t, err, "Failed to create parser") - - handlers, err := parser.ExtractHandlers("Resolver") - require.NoError(t, err, "Failed to extract handlers") - - handler := handlers["HandleTest"] - // After transformation, toolResolver should be changed to Resolver - if !strings.Contains(handler.SourceCode, "func (r *Resolver) HandleTest") { - t.Errorf("Source code should contain transformed function signature, got: %s", handler.SourceCode) - } - assert.Contains(t, handler.SourceCode, "return nil", "Source code should contain function body") - assert.Contains(t, handler.SourceCode, "x := 42", "Source code should contain function body statements") -} diff --git a/third_party/mcpgen/internal/codegen/templates/resolver.gotpl b/third_party/mcpgen/internal/codegen/templates/resolver.gotpl deleted file mode 100644 index 66f3332ec..000000000 --- a/third_party/mcpgen/internal/codegen/templates/resolver.gotpl +++ /dev/null @@ -1,58 +0,0 @@ -package {{.Package}} - -// This file will be automatically regenerated based on the schema, any resolver implementations -// will be copied through when generating and any unknown code will be moved to the end. -// Code generated by mcpgen. DO NOT EDIT. - -import ( - "context" - "fmt" - - "github.com/modelcontextprotocol/go-sdk/mcp" - {{- if .Imports}} - {{- range .Imports}} - {{- if .Alias}} - {{.Alias}} "{{.Path}}" - {{- else}} - "{{.Path}}" - {{- end}} - {{- end}} - {{- end}} -) - -{{- range .Tools}} - -{{- if .HasInputType}} -func (r *{{$.ResolverType}}) {{.HandlerName}}Tool(ctx context.Context, req *mcp.CallToolRequest, input *{{.InputType}}) (*mcp.CallToolResult, {{if .HasOutputType}}{{.OutputType}}{{else}}map[string]any{{end}}, error) { - return nil, {{if .HasOutputType}}{{.OutputType}}{}{{else}}nil{{end}}, fmt.Errorf("{{.Name}} not implemented") -} -{{- else}} -func (r *{{$.ResolverType}}) {{.HandlerName}}Tool(ctx context.Context, req *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, {{if .HasOutputType}}{{.OutputType}}{{else}}map[string]any{{end}}, error) { - return nil, {{if .HasOutputType}}{{.OutputType}}{}{{else}}nil{{end}}, fmt.Errorf("{{.Name}} not implemented") -} -{{- end}} -{{- end}} - -{{- if .HasResources}} -{{- range .Resources}} - -func (r *{{$.ResolverType}}) {{.HandlerName}}Resource(ctx context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { - return nil, fmt.Errorf("{{.Name}} not implemented") -} -{{- end}} -{{- end}} - -{{- if .HasPrompts}} -{{- range .Prompts}} - -{{- if .HasArgsType}} -func (r *{{$.ResolverType}}) {{.HandlerName}}Prompt(ctx context.Context, req *mcp.GetPromptRequest, args {{.ArgsType}}) (*mcp.GetPromptResult, error) { - return nil, fmt.Errorf("{{.Name}} not implemented") -} -{{- else}} -func (r *{{$.ResolverType}}) {{.HandlerName}}Prompt(ctx context.Context, req *mcp.GetPromptRequest, args map[string]string) (*mcp.GetPromptResult, error) { - return nil, fmt.Errorf("{{.Name}} not implemented") -} -{{- end}} -{{- end}} -{{- end}} diff --git a/third_party/mcpgen/internal/codegen/templates/resolver_struct.gotpl b/third_party/mcpgen/internal/codegen/templates/resolver_struct.gotpl deleted file mode 100644 index f07f63adf..000000000 --- a/third_party/mcpgen/internal/codegen/templates/resolver_struct.gotpl +++ /dev/null @@ -1,22 +0,0 @@ -package {{.Package}} - -// This file will NOT be regenerated automatically. -// -// It serves as a dependency injection container for your resolvers. -// Add any dependencies you need here (database connections, API clients, etc.) -// and they'll be available to all your tool, prompt, and resource resolvers. - -// {{.ResolverType}} is the root resolver that holds dependencies for all MCP handlers -type {{.ResolverType}} struct { - // Add your dependencies here, for example: - // DB *sql.DB - // Cache *redis.Client - // APIClient *http.Client -} - -// New{{.ResolverType}} creates a new resolver instance -func New{{.ResolverType}}() *{{.ResolverType}} { - return &{{.ResolverType}}{ - // Initialize your dependencies here - } -} diff --git a/third_party/mcpgen/internal/codegen/templates/server.gotpl b/third_party/mcpgen/internal/codegen/templates/server.gotpl deleted file mode 100644 index 27afcb6f1..000000000 --- a/third_party/mcpgen/internal/codegen/templates/server.gotpl +++ /dev/null @@ -1,175 +0,0 @@ -// Code generated by mcpgen. DO NOT EDIT. - -package {{.Package}} - -import ( - "context" - "github.com/modelcontextprotocol/go-sdk/mcp" - {{- if .Imports}} - {{- range .Imports}} - {{- if .Alias}} - {{.Alias}} "{{.Path}}" - {{- else}} - "{{.Path}}" - {{- end}} - {{- end}} - {{- end}} - mcputil "go.probo.inc/mcpgen/mcp" -) - -// ResolverInterface defines the interface that must be implemented by the parent resolver -type ResolverInterface interface { - {{- range .Tools}} - {{.HandlerName}}Tool(ctx context.Context, req *mcp.CallToolRequest{{if .HasInputType}}, input *{{.InputType}}{{else}}, args map[string]any{{end}}) (*mcp.CallToolResult, {{if .HasOutputType}}{{.OutputType}}{{else}}map[string]any{{end}}, error) - {{- end}} - {{- if .HasResources}} - {{- range .Resources}} - {{.HandlerName}}Resource(ctx context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) - {{- end}} - {{- end}} - {{- if .HasPrompts}} - {{- range .Prompts}} - {{.HandlerName}}Prompt(ctx context.Context, req *mcp.GetPromptRequest{{if .HasArgsType}}, args {{.ArgsType}}{{else}}, args map[string]string{{end}}) (*mcp.GetPromptResult, error) - {{- end}} - {{- end}} -} - -// New creates a new MCP server instance with all handlers registered. -// Returns a fully configured *mcp.Server ready to be used with any transport. -func New(resolver ResolverInterface, opts ...mcputil.Option) *mcp.Server { - o := mcputil.ApplyOptions(opts) - - server := mcp.NewServer( - &mcp.Implementation{ - Name: "{{.ServerName}}", - Version: "{{.ServerVersion}}", - }, - nil, - ) - - registerToolHandlers(server, resolver, &o) - {{- if .HasResources}} - registerResourceHandlers(server, resolver) - {{- end}} - {{- if .HasPrompts}} - registerPromptHandlers(server, resolver) - {{- end}} - - return server -} - -func registerToolHandlers(server *mcp.Server, resolver ResolverInterface, opts *mcputil.Options) { - {{- range .Tools}} - {{- $hasAnnotations := or .HasHints .Title}} - mcp.AddTool( - server, - &mcp.Tool{ - Name: "{{.Name}}", - {{- if .Title}} - Title: "{{.Title}}", - {{- end}} - Description: "{{.Description}}", - {{- if .HasInputType}} - InputSchema: {{.InputSchemaVar}}, - {{- end}} - {{- if .HasOutputType}} - OutputSchema: {{.OutputSchemaVar}}, - {{- end}} - {{- if $hasAnnotations}} - Annotations: &mcp.ToolAnnotations{ - {{- if .Title}} - Title: "{{.Title}}", - {{- end}} - {{- if .Readonly}} - ReadOnlyHint: true, - {{- else if .HasHints}} - ReadOnlyHint: false, - DestructiveHint: boolPtr({{if .Destructive}}true{{else}}false{{end}}), - {{- end}} - {{- if .Idempotent}} - IdempotentHint: true, - {{- end}} - {{- if .OpenWorld}} - OpenWorldHint: boolPtr(true), - {{- end}} - }, - {{- end}} - }, - func(ctx context.Context, req *mcp.CallToolRequest, input {{if .HasInputType}}*{{.InputType}}{{else}}map[string]any{{end}}) (result *mcp.CallToolResult, output {{if .HasOutputType}}{{.OutputType}}{{else}}map[string]any{{end}}, err error) { - defer func() { - if r := recover(); r != nil { - err = opts.RecoverFunc(ctx, r) - } - }() - return resolver.{{.HandlerName}}Tool(ctx, req, input) - }, - ) - - {{- end}} -} - -func boolPtr(b bool) *bool { - return &b -} - -{{- if .HasResources}} - -func registerResourceHandlers(server *mcp.Server, resolver ResolverInterface) { - {{- range .Resources}} - {{- if .URI}} - server.AddResource( - &mcp.Resource{ - URI: "{{.URI}}", - Name: "{{.Name}}", - Description: "{{.Description}}", - {{- if .MimeType}} - MIMEType: "{{.MimeType}}", - {{- end}} - }, - resolver.{{.HandlerName}}Resource, - ) - - {{- else if .URITemplate}} - server.AddResourceTemplate( - &mcp.ResourceTemplate{ - URITemplate: "{{.URITemplate}}", - Name: "{{.Name}}", - Description: "{{.Description}}", - {{- if .MimeType}} - MIMEType: "{{.MimeType}}", - {{- end}} - }, - resolver.{{.HandlerName}}Resource, - ) - - {{- end}} - {{- end}} -} -{{- end}} - -{{- if .HasPrompts}} - -func registerPromptHandlers(server *mcp.Server, resolver ResolverInterface) { - {{- range .Prompts}} - mcputil.AddPrompt( - server, - &mcp.Prompt{ - Name: "{{.Name}}", - Description: "{{.Description}}", - {{- if .Arguments}} - Arguments: []*mcp.PromptArgument{ - {{- range .Arguments}} - { - Name: "{{.Name}}", - Description: "{{.Description}}", - Required: {{.Required}}, - }, - {{- end}} - }, - {{- end}} - }, - resolver.{{.HandlerName}}Prompt, - ) - {{- end}} -} -{{- end}} diff --git a/third_party/mcpgen/internal/codegen/testdata/all_primitives.yaml b/third_party/mcpgen/internal/codegen/testdata/all_primitives.yaml deleted file mode 100644 index 3f95f95b0..000000000 --- a/third_party/mcpgen/internal/codegen/testdata/all_primitives.yaml +++ /dev/null @@ -1,222 +0,0 @@ -info: - title: all-primitives-test - version: 1.0.0 - description: Test all MCP primitives and JSON Schema features - -components: - schemas: - # All JSON Schema types - StringSchema: - type: string - description: A string - - NumberSchema: - type: number - description: A number - - IntegerSchema: - type: integer - description: An integer - - BooleanSchema: - type: boolean - description: A boolean - - ArraySchema: - type: array - items: - type: string - description: An array of strings - - ObjectSchema: - type: object - properties: - name: - type: string - value: - type: number - required: [name] - - # Enum types - Color: - type: string - enum: [red, green, blue, yellow] - description: A color - - # Nested objects - Address: - type: object - properties: - street: - type: string - city: - type: string - zipCode: - type: string - country: - type: string - required: [city, country] - - Person: - type: object - properties: - name: - type: string - age: - type: integer - email: - type: string - format: email - address: - $ref: "#/components/schemas/Address" - favoriteColor: - $ref: "#/components/schemas/Color" - tags: - type: array - items: - type: string - metadata: - type: object - additionalProperties: true - required: [name] - - # Nullable fields with anyOf - NullableFields: - type: object - properties: - nullableString: - anyOf: - - type: string - - type: "null" - nullableNumber: - anyOf: - - type: number - - type: "null" - nullableObject: - anyOf: - - $ref: "#/components/schemas/Address" - - type: "null" - - # Complex nested structure - Organization: - type: object - properties: - id: - type: string - name: - type: string - members: - type: array - items: - $ref: "#/components/schemas/Person" - headquarters: - $ref: "#/components/schemas/Address" - founded: - type: string - format: date - required: [id, name] - -tools: - # Tool with inline schema - - name: simple_tool - description: A simple tool with inline schema - inputSchema: - type: object - properties: - message: - type: string - count: - type: integer - required: [message] - - # Tool with ref schema - - name: create_person - description: Create a person - inputSchema: - $ref: "#/components/schemas/Person" - - # Tool with complex schema - - name: create_organization - description: Create an organization - inputSchema: - $ref: "#/components/schemas/Organization" - - # Tool with enum - - name: set_color - description: Set a color - inputSchema: - type: object - properties: - color: - $ref: "#/components/schemas/Color" - required: [color] - - # Tool with nullable fields - - name: update_fields - description: Update optional fields - inputSchema: - $ref: "#/components/schemas/NullableFields" - -resources: - # Static resource - - uri: "docs://readme" - name: README - description: The README document - mimeType: text/markdown - readonly: true - - # Resource with simple template - - uriTemplate: "person://{id}" - name: Person Resource - description: Get a person by ID - mimeType: application/json - readonly: true - schema: - $ref: "#/components/schemas/Person" - - # Resource with multiple parameters - - uriTemplate: "org://{orgId}/member/{memberId}" - name: Organization Member - description: Get a member of an organization - mimeType: application/json - readonly: true - schema: - $ref: "#/components/schemas/Person" - - # Resource with nested schema - - uriTemplate: "org://{id}" - name: Organization Resource - description: Get an organization by ID - mimeType: application/json - schema: - $ref: "#/components/schemas/Organization" - -prompts: - # Prompt without arguments - - name: help - description: Get general help - - # Prompt with optional arguments - - name: person_info - description: Get information about a person - arguments: - - name: personId - description: The person ID - required: true - - name: includeAddress - description: Include address in the response - required: false - - name: format - description: Output format - required: false - - # Prompt with all required arguments - - name: compare_people - description: Compare two people - arguments: - - name: person1Id - description: First person ID - required: true - - name: person2Id - description: Second person ID - required: true diff --git a/third_party/mcpgen/internal/codegen/testdata/config_based_types.golden b/third_party/mcpgen/internal/codegen/testdata/config_based_types.golden deleted file mode 100644 index ef3e5f38b..000000000 --- a/third_party/mcpgen/internal/codegen/testdata/config_based_types.golden +++ /dev/null @@ -1,31 +0,0 @@ -// Code generated by mcpgen. DO NOT EDIT. - -package test - -import ( - "github.com/google/uuid" - "github.com/myapp/models" - mcputil "go.probo.inc/mcpgen/mcp" - "time" -) - -// Tool input schemas -var ( - CreateEventToolInputSchema = mcputil.MustUnmarshalSchema(`{"type":"object","required":["id","name","createdAt"],"properties":{"createdAt":{"$ref":"#/components/schemas/Timestamp"},"id":{"$ref":"#/components/schemas/UUID"},"name":{"type":"string"},"owner":{"$ref":"#/components/schemas/User"}}}`) -) - -// Event represents the schema -type Event struct { - Name string `json:"name"` - Owner models.User `json:"owner,omitempty"` - CreatedAt time.Time `json:"createdAt"` - ID uuid.UUID `json:"id"` -} - -// CreateEventInput represents the schema -type CreateEventInput struct { - Name string `json:"name"` - Owner models.User `json:"owner,omitempty"` - CreatedAt time.Time `json:"createdAt"` - ID uuid.UUID `json:"id"` -} diff --git a/third_party/mcpgen/internal/codegen/testdata/config_based_types.yaml b/third_party/mcpgen/internal/codegen/testdata/config_based_types.yaml deleted file mode 100644 index b92a37d07..000000000 --- a/third_party/mcpgen/internal/codegen/testdata/config_based_types.yaml +++ /dev/null @@ -1,45 +0,0 @@ -info: - title: config-based-test - version: 1.0.0 - description: Test config-based custom type mapping - -components: - schemas: - # These will be mapped via config, not go.probo.inc/mcpgen/type - Timestamp: - type: string - format: date-time - - UUID: - type: string - format: uuid - - User: - type: object - properties: - id: - type: string - name: - type: string - email: - type: string - format: email - required: [id, name] - - Event: - type: object - properties: - id: - $ref: "#/components/schemas/UUID" - name: - type: string - owner: - $ref: "#/components/schemas/User" - createdAt: - $ref: "#/components/schemas/Timestamp" - required: [id, name, createdAt] - -tools: - - name: create_event - inputSchema: - $ref: "#/components/schemas/Event" diff --git a/third_party/mcpgen/internal/codegen/testdata/custom_types.yaml b/third_party/mcpgen/internal/codegen/testdata/custom_types.yaml deleted file mode 100644 index a0f112bce..000000000 --- a/third_party/mcpgen/internal/codegen/testdata/custom_types.yaml +++ /dev/null @@ -1,185 +0,0 @@ -info: - title: custom-types-test - version: 1.0.0 - description: Test all custom type mapping scenarios - -components: - schemas: - # Standard library types with go.probo.inc/mcpgen/type - Timestamp: - type: string - format: date-time - description: A timestamp - go.probo.inc/mcpgen/type: time.Time - - Duration: - type: string - description: A duration - go.probo.inc/mcpgen/type: time.Duration - - # External package types - UUID: - type: string - format: uuid - description: A UUID - go.probo.inc/mcpgen/type: github.com/google/uuid.UUID - - Decimal: - type: string - description: A decimal number - go.probo.inc/mcpgen/type: github.com/shopspring/decimal.Decimal - - # JSON raw message - Metadata: - type: object - description: Raw JSON metadata - go.probo.inc/mcpgen/type: json.RawMessage - - # Regular enum (should be generated) - Status: - type: string - enum: [pending, in_progress, completed, cancelled] - description: Task status - - # Regular object (should be generated) - Task: - type: object - description: A task - properties: - id: - $ref: "#/components/schemas/UUID" - title: - type: string - description: Task title - status: - $ref: "#/components/schemas/Status" - createdAt: - $ref: "#/components/schemas/Timestamp" - updatedAt: - anyOf: - - $ref: "#/components/schemas/Timestamp" - - type: "null" - duration: - $ref: "#/components/schemas/Duration" - metadata: - $ref: "#/components/schemas/Metadata" - tags: - type: array - items: - type: string - description: Task tags - priority: - type: integer - description: Priority level - required: [id, title, status, createdAt] - - # Object with all nullable custom types - OptionalFields: - type: object - properties: - optionalTimestamp: - anyOf: - - $ref: "#/components/schemas/Timestamp" - - type: "null" - optionalUUID: - anyOf: - - $ref: "#/components/schemas/UUID" - - type: "null" - optionalDecimal: - anyOf: - - $ref: "#/components/schemas/Decimal" - - type: "null" - - # Nested objects - Project: - type: object - properties: - id: - $ref: "#/components/schemas/UUID" - name: - type: string - tasks: - type: array - items: - $ref: "#/components/schemas/Task" - createdAt: - $ref: "#/components/schemas/Timestamp" - required: [id, name, createdAt] - - # Update input with omittable fields - UpdateTaskInput: - type: object - description: Input for partial task update - properties: - id: - $ref: "#/components/schemas/UUID" - description: Task ID to update - title: - anyOf: - - type: string - - type: "null" - description: New title (omit to keep unchanged, null to clear) - go.probo.inc/mcpgen/omittable: true - status: - anyOf: - - $ref: "#/components/schemas/Status" - - type: "null" - description: New status (omit to keep unchanged) - go.probo.inc/mcpgen/omittable: true - priority: - anyOf: - - type: integer - - type: "null" - description: New priority (omit to keep unchanged, null to clear) - go.probo.inc/mcpgen/omittable: true - tags: - anyOf: - - type: array - items: - type: string - - type: "null" - description: New tags (omit to keep unchanged, null to clear) - go.probo.inc/mcpgen/omittable: true - required: [id] - -tools: - - name: create_task - description: Create a new task - inputSchema: - $ref: "#/components/schemas/Task" - - - name: update_task - description: Update task fields (partial update with omittable fields) - inputSchema: - $ref: "#/components/schemas/UpdateTaskInput" - - - name: create_project - description: Create a new project - inputSchema: - $ref: "#/components/schemas/Project" - -resources: - - uriTemplate: "task://{id}" - name: Task Resource - description: Get a task by ID - mimeType: application/json - schema: - $ref: "#/components/schemas/Task" - - - uriTemplate: "project://{id}" - name: Project Resource - description: Get a project by ID - mimeType: application/json - schema: - $ref: "#/components/schemas/Project" - -prompts: - - name: task_summary - description: Generate a task summary - arguments: - - name: taskId - description: The task ID - required: true - - name: includeMetadata - description: Include metadata in summary - required: false diff --git a/third_party/mcpgen/internal/codegen/types.go b/third_party/mcpgen/internal/codegen/types.go deleted file mode 100644 index af0d58425..000000000 --- a/third_party/mcpgen/internal/codegen/types.go +++ /dev/null @@ -1,652 +0,0 @@ -package codegen - -import ( - "fmt" - "go/format" - "sort" - "strings" - - "go.probo.inc/mcpgen/internal/schema" -) - -type CustomTypeMapping struct { - GoType string - ImportPath string - IsPointer bool -} - -type TypeGenerator struct { - schemas map[string]*schema.Schema - types map[string]string - enums map[string]string - imports map[string]bool - schemaVars map[string]string - customMappings map[string]*CustomTypeMapping -} - -func NewTypeGenerator() *TypeGenerator { - return &TypeGenerator{ - schemas: make(map[string]*schema.Schema), - types: make(map[string]string), - enums: make(map[string]string), - imports: make(map[string]bool), - schemaVars: make(map[string]string), - customMappings: make(map[string]*CustomTypeMapping), - } -} - -func (g *TypeGenerator) AddCustomMapping(schemaName string, mapping *CustomTypeMapping) { - g.customMappings[schemaName] = mapping -} - -func (g *TypeGenerator) AddSchema(name string, s *schema.Schema) { - g.schemas[name] = s -} - -func (g *TypeGenerator) AddSchemaVar(name string, schemaJSON string) { - g.schemaVars[name] = schemaJSON - g.imports["go.probo.inc/mcpgen/mcp"] = true -} - -func (g *TypeGenerator) Generate(packageName string) ([]byte, error) { - var buf strings.Builder - - buf.WriteString("// Code generated by mcpgen. DO NOT EDIT.\n\n") - buf.WriteString(fmt.Sprintf("package %s\n\n", packageName)) - - // Sort schema names for deterministic output - schemaNames := make([]string, 0, len(g.schemas)) - for name := range g.schemas { - schemaNames = append(schemaNames, name) - } - sort.Strings(schemaNames) - - for _, name := range schemaNames { - s := g.schemas[name] - typeName := toGoTypeName(name) - - if _, hasCustomMapping := g.customMappings[name]; hasCustomMapping { - continue - } - - typeCode, err := g.generateType(typeName, s, 0) - if err != nil { - return nil, fmt.Errorf("failed to generate type for %s: %w", name, err) - } - - if typeCode != "" && g.types[typeName] == "" { - g.types[typeName] = typeCode - } - } - - if len(g.imports) > 0 { - buf.WriteString("import (\n") - // Sort imports for deterministic output - imports := make([]string, 0, len(g.imports)) - for imp := range g.imports { - imports = append(imports, imp) - } - sort.Strings(imports) - for _, imp := range imports { - buf.WriteString(fmt.Sprintf("\t\"%s\"\n", imp)) - } - buf.WriteString(")\n\n") - } - - if len(g.schemaVars) > 0 { - buf.WriteString("// Tool input schemas\n") - buf.WriteString("var (\n") - // Sort schema var names for deterministic output - varNames := make([]string, 0, len(g.schemaVars)) - for varName := range g.schemaVars { - varNames = append(varNames, varName) - } - sort.Strings(varNames) - for _, varName := range varNames { - schemaJSON := g.schemaVars[varName] - buf.WriteString(fmt.Sprintf("\t%s = mcp.MustUnmarshalSchema(`%s`)\n", varName, schemaJSON)) - } - buf.WriteString(")\n\n") - } - - // Sort enum names for deterministic output - enumNames := make([]string, 0, len(g.enums)) - for enumName := range g.enums { - enumNames = append(enumNames, enumName) - } - sort.Strings(enumNames) - for _, enumName := range enumNames { - enumCode := g.enums[enumName] - buf.WriteString(enumCode) - buf.WriteString("\n\n") - } - - written := make(map[string]bool) - - // Sort schema names for deterministic output (second pass) - for _, name := range schemaNames { - typeName := toGoTypeName(name) - if typeCode := g.types[typeName]; typeCode != "" { - buf.WriteString(typeCode) - buf.WriteString("\n\n") - written[typeName] = true - } - } - - // Sort type names for deterministic output - typeNames := make([]string, 0, len(g.types)) - for typeName := range g.types { - typeNames = append(typeNames, typeName) - } - sort.Strings(typeNames) - for _, typeName := range typeNames { - typeCode := g.types[typeName] - if !written[typeName] && typeCode != "" { - buf.WriteString(typeCode) - buf.WriteString("\n\n") - } - } - - formatted, err := format.Source([]byte(buf.String())) - if err != nil { - return nil, fmt.Errorf("failed to format generated code: %w\n%s", err, buf.String()) - } - - return formatted, nil -} - -func (g *TypeGenerator) generateType(name string, s *schema.Schema, depth int) (string, error) { - schemaType := schema.GetType(s) - - if schemaType == "" && s.Properties != nil && len(s.Properties) > 0 { - return g.generateStruct(name, s, depth) - } - - if schemaType == "" && s.Properties == nil { - return "", fmt.Errorf("unsupported schema type: %q (no type and no properties for %s)", schemaType, name) - } - - if len(s.Enum) > 0 { - return g.generateEnum(name, s) - } - - switch schemaType { - case "object": - return g.generateStruct(name, s, depth) - case "array": - return g.generateArrayType(name, s, depth) - case "string": - if depth == 0 { - return g.generatePrimitiveTypeAlias(name, s, "string") - } - return "", nil - case "number": - if depth == 0 { - return g.generatePrimitiveTypeAlias(name, s, "float64") - } - return "", nil - case "integer": - if depth == 0 { - return g.generatePrimitiveTypeAlias(name, s, "int") - } - return "", nil - case "boolean": - if depth == 0 { - return g.generatePrimitiveTypeAlias(name, s, "bool") - } - return "", nil - default: - if len(s.Properties) > 0 { - return g.generateStruct(name, s, depth) - } - return "", fmt.Errorf("unsupported schema type: %s", schemaType) - } -} - -func (g *TypeGenerator) generateStruct(name string, s *schema.Schema, depth int) (string, error) { - var buf strings.Builder - - if s.Description != "" { - buf.WriteString(formatComment(s.Description, "")) - } else if s.Title != "" { - buf.WriteString(formatComment(s.Title, "")) - } else { - buf.WriteString(fmt.Sprintf("// %s represents the schema\n", name)) - } - - buf.WriteString(fmt.Sprintf("type %s struct {\n", name)) - - // Sort property names for deterministic output - propNames := make([]string, 0, len(s.Properties)) - for propName := range s.Properties { - propNames = append(propNames, propName) - } - sort.Strings(propNames) - - for _, propName := range propNames { - propSchema := s.Properties[propName] - fieldName := toGoFieldName(propName) - hint := name + fieldName - - isRequired := schema.IsRequired(s, propName) - isOmittable := schema.IsOmittable(propSchema) - - // Validate that omittable is only used on nullable fields - if isOmittable { - isNullable, _ := isNullableType(propSchema) - if !isNullable { - return "", fmt.Errorf("field %s.%s has omittable annotation but is not nullable (omittable only works with nullable fields)", name, propName) - } - } - - fieldType, err := g.goType(propSchema, hint) - if err != nil { - return "", fmt.Errorf("failed to generate field %s: %w", propName, err) - } - - if isOmittable { - fieldType = fmt.Sprintf("mcp.Omittable[%s]", fieldType) - g.imports["go.probo.inc/mcpgen/mcp"] = true - } else if !isRequired && !isPointerType(fieldType) { - fieldType = "*" + fieldType - } - - if propSchema.Description != "" { - buf.WriteString(formatComment(propSchema.Description, "\t")) - } - - buf.WriteString(fmt.Sprintf("\t%s %s", fieldName, fieldType)) - - jsonTag := propName - if !isRequired { - jsonTag += ",omitempty" - } - buf.WriteString(fmt.Sprintf(" `json:\"%s\"`", jsonTag)) - - buf.WriteString("\n") - } - - buf.WriteString("}") - - return buf.String(), nil -} - -// isPointerType checks if the given type string is already a pointer or slice type -func isPointerType(t string) bool { - return len(t) > 0 && (t[0] == '*' || t[0] == '[') -} - -func isNullableType(s *schema.Schema) (bool, *schema.Schema) { - if len(s.AnyOf) == 2 { - var nullIndex = -1 - var typeIndex = -1 - - for i, subSchema := range s.AnyOf { - subType := schema.GetType(subSchema) - if subType == "null" { - nullIndex = i - } else if subType != "" || subSchema.Properties != nil || subSchema.Ref != "" { - typeIndex = i - } - } - - if nullIndex >= 0 && typeIndex >= 0 { - return true, s.AnyOf[typeIndex] - } - } - - if len(s.Types) > 0 { - hasNull := false - var otherType string - for _, t := range s.Types { - if t == "null" { - hasNull = true - } else if otherType == "" { - otherType = t - } - } - - if hasNull && otherType != "" && len(s.Types) == 2 { - syntheticSchema := &schema.Schema{ - Type: otherType, - Format: s.Format, - } - return true, syntheticSchema - } - } - - return false, nil -} - -func (g *TypeGenerator) generateArrayType(name string, s *schema.Schema, depth int) (string, error) { - if s.Items == nil { - if depth == 0 { - return g.generatePrimitiveTypeAlias(name, s, "[]any") - } - return "[]any", nil - } - - itemType, err := g.goType(s.Items, name+"Item") - if err != nil { - return "", err - } - - arrayType := fmt.Sprintf("[]%s", itemType) - - if depth == 0 { - return g.generatePrimitiveTypeAlias(name, s, arrayType) - } - - return arrayType, nil -} - -func (g *TypeGenerator) goType(s *schema.Schema, hint string) (string, error) { - if s.Ref != "" { - const prefix = "#/components/schemas/" - if len(s.Ref) > len(prefix) && s.Ref[:len(prefix)] == prefix { - schemaName := s.Ref[len(prefix):] - - if customMapping, ok := g.customMappings[schemaName]; ok { - if customMapping.ImportPath != "" { - g.imports[customMapping.ImportPath] = true - } - if customMapping.IsPointer { - return "*" + customMapping.GoType, nil - } - return customMapping.GoType, nil - } - - return "*" + toGoTypeName(schemaName), nil - } - } - - if nullable, baseType := isNullableType(s); nullable { - goType, err := g.goType(baseType, hint) - if err != nil { - return "", err - } - if len(goType) > 0 && goType[0] == '*' { - return goType, nil - } - return "*" + goType, nil - } - - schemaType := schema.GetType(s) - - switch schemaType { - case "string": - if len(s.Enum) > 0 { - enumTypeName := toGoTypeName(hint) - if g.enums[enumTypeName] == "" { - enumCode, err := g.generateEnum(enumTypeName, s) - if err != nil { - return "", err - } - g.enums[enumTypeName] = enumCode - } - return enumTypeName, nil - } - return g.goStringType(s), nil - case "number": - return "float64", nil - case "integer": - return "int", nil - case "boolean": - return "bool", nil - case "array": - if s.Items == nil { - return "[]any", nil - } - itemType, err := g.goType(s.Items, hint+"Item") - if err != nil { - return "", err - } - return fmt.Sprintf("[]%s", itemType), nil - case "object": - if s.Title != "" { - typeName := toGoTypeName(s.Title) - if g.types[typeName] == "" { - typeCode, err := g.generateStruct(typeName, s, 0) - if err != nil { - return "", err - } - g.types[typeName] = typeCode - } - return typeName, nil - } - if len(s.Properties) > 0 { - typeName := hint - if g.types[typeName] == "" { - typeCode, err := g.generateStruct(typeName, s, 0) - if err != nil { - return "", err - } - g.types[typeName] = typeCode - } - return typeName, nil - } - return "map[string]any", nil - case "null": - return "any", nil - default: - if len(s.Properties) > 0 { - typeName := toGoTypeName(hint) - if g.types[typeName] == "" { - typeCode, err := g.generateStruct(typeName, s, 0) - if err != nil { - return "", err - } - g.types[typeName] = typeCode - } - return typeName, nil - } - return "any", nil - } -} - -func (g *TypeGenerator) generatePrimitiveTypeAlias(name string, s *schema.Schema, goType string) (string, error) { - var buf strings.Builder - - if s.Description != "" { - buf.WriteString(formatComment(s.Description, "")) - } else { - buf.WriteString(fmt.Sprintf("// %s represents a %s schema\n", name, goType)) - } - - buf.WriteString(fmt.Sprintf("type %s %s", name, goType)) - return buf.String(), nil -} - -func (g *TypeGenerator) generateEnum(enumTypeName string, s *schema.Schema) (string, error) { - if len(s.Enum) == 0 { - return "", fmt.Errorf("schema has no enum values") - } - - var buf strings.Builder - - if s.Description != "" { - buf.WriteString(formatComment(s.Description, "")) - } else { - buf.WriteString(fmt.Sprintf("// %s represents an enumeration\n", enumTypeName)) - } - - buf.WriteString(fmt.Sprintf("type %s string\n\n", enumTypeName)) - - buf.WriteString("const (\n") - var enumValues []string - for i, enumValue := range s.Enum { - strValue := fmt.Sprintf("%v", enumValue) - enumValues = append(enumValues, strValue) - constName := toEnumConstName(enumTypeName, strValue) - - if i == 0 { - buf.WriteString(fmt.Sprintf("\t%s %s = %q\n", constName, enumTypeName, strValue)) - } else { - buf.WriteString(fmt.Sprintf("\t%s %s = %q\n", constName, enumTypeName, strValue)) - } - } - buf.WriteString(")\n\n") - - // Generate validation method - buf.WriteString(fmt.Sprintf("// IsValid returns true if the %s value is valid\n", enumTypeName)) - buf.WriteString(fmt.Sprintf("func (e %s) IsValid() bool {\n", enumTypeName)) - buf.WriteString("\tswitch e {\n") - for _, strValue := range enumValues { - constName := toEnumConstName(enumTypeName, strValue) - buf.WriteString(fmt.Sprintf("\tcase %s:\n\t\treturn true\n", constName)) - } - buf.WriteString("\t}\n") - buf.WriteString("\treturn false\n") - buf.WriteString("}\n\n") - - // Generate UnmarshalJSON method - buf.WriteString("// UnmarshalJSON implements json.Unmarshaler\n") - buf.WriteString(fmt.Sprintf("func (e *%s) UnmarshalJSON(data []byte) error {\n", enumTypeName)) - buf.WriteString("\tvar s string\n") - buf.WriteString("\tif err := json.Unmarshal(data, &s); err != nil {\n") - buf.WriteString("\t\treturn err\n") - buf.WriteString("\t}\n") - buf.WriteString(fmt.Sprintf("\t*e = %s(s)\n", enumTypeName)) - buf.WriteString("\tif !e.IsValid() {\n") - buf.WriteString(fmt.Sprintf("\t\treturn fmt.Errorf(\"invalid %s value: %%q\", s)\n", enumTypeName)) - buf.WriteString("\t}\n") - buf.WriteString("\treturn nil\n") - buf.WriteString("}\n\n") - - // Generate MarshalJSON method - buf.WriteString("// MarshalJSON implements json.Marshaler\n") - buf.WriteString(fmt.Sprintf("func (e %s) MarshalJSON() ([]byte, error) {\n", enumTypeName)) - buf.WriteString("\tif !e.IsValid() {\n") - buf.WriteString(fmt.Sprintf("\t\treturn nil, fmt.Errorf(\"invalid %s value: %%q\", string(e))\n", enumTypeName)) - buf.WriteString("\t}\n") - buf.WriteString("\treturn json.Marshal(string(e))\n") - buf.WriteString("}") - - g.imports["encoding/json"] = true - g.imports["fmt"] = true - - return buf.String(), nil -} - -func (g *TypeGenerator) goStringType(s *schema.Schema) string { - switch s.Format { - case "date-time": - g.imports["time"] = true - return "time.Time" - case "date", "time", "email", "hostname", "ipv4", "ipv6", "uri", "uuid": - return "string" - default: - return "string" - } -} - -func toGoTypeName(name string) string { - name = strings.TrimSuffix(name, ".json") - name = strings.TrimSuffix(name, "_input") - name = strings.TrimSuffix(name, "_output") - name = strings.TrimSuffix(name, "_schema") - - parts := strings.FieldsFunc(name, func(r rune) bool { - return r == '_' || r == '-' || r == ' ' || r == '.' - }) - - for i, part := range parts { - if len(part) > 0 { - parts[i] = strings.ToUpper(part[:1]) + part[1:] - } - } - - return strings.Join(parts, "") -} - -var goAcronyms = map[string]bool{ - "acl": true, - "api": true, - "ascii": true, - "cpu": true, - "css": true, - "dns": true, - "eof": true, - "guid": true, - "html": true, - "http": true, - "https": true, - "id": true, - "ip": true, - "json": true, - "jwt": true, - "lhs": true, - "qps": true, - "ram": true, - "rhs": true, - "rpc": true, - "sla": true, - "smtp": true, - "sql": true, - "ssh": true, - "tcp": true, - "tls": true, - "ttl": true, - "udp": true, - "ui": true, - "uid": true, - "uri": true, - "url": true, - "utf": true, - "uuid": true, - "vm": true, - "xml": true, -} - -var goSpecialCase = map[string]string{ - "oauth": "OAuth", -} - -func toGoFieldName(name string) string { - parts := strings.FieldsFunc(name, func(r rune) bool { - return r == '_' || r == '-' || r == ' ' - }) - - for i, part := range parts { - if len(part) > 0 { - lowerPart := strings.ToLower(part) - if specialCase, ok := goSpecialCase[lowerPart]; ok { - parts[i] = specialCase - } else if goAcronyms[lowerPart] { - parts[i] = strings.ToUpper(part) - } else { - parts[i] = strings.ToUpper(part[:1]) + part[1:] - } - } - } - - return strings.Join(parts, "") -} - -func formatComment(text, prefix string) string { - lines := strings.Split(strings.TrimSpace(text), "\n") - var result strings.Builder - - for _, line := range lines { - result.WriteString(fmt.Sprintf("%s// %s\n", prefix, strings.TrimSpace(line))) - } - - return result.String() -} - -func toEnumConstName(enumTypeName, value string) string { - parts := strings.FieldsFunc(value, func(r rune) bool { - return r == '_' || r == '-' || r == ' ' || r == '.' - }) - - for i, part := range parts { - if len(part) > 0 { - parts[i] = strings.ToUpper(part[:1]) + part[1:] - } - } - - constName := strings.Join(parts, "") - baseName := strings.TrimSuffix(enumTypeName, "Type") - - return baseName + constName -} diff --git a/third_party/mcpgen/internal/codegen/types_test.go b/third_party/mcpgen/internal/codegen/types_test.go deleted file mode 100644 index 33aeca3c9..000000000 --- a/third_party/mcpgen/internal/codegen/types_test.go +++ /dev/null @@ -1,1048 +0,0 @@ -package codegen - -import ( - "testing" - - "github.com/google/jsonschema-go/jsonschema" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.probo.inc/mcpgen/internal/config" -) - -func TestParseTypeMapping(t *testing.T) { - tests := []struct { - name string - input string - wantGoType string - wantImportPath string - }{ - { - name: "built-in type", - input: "string", - wantGoType: "string", - wantImportPath: "", - }, - { - name: "standard library type", - input: "time.Time", - wantGoType: "time.Time", - wantImportPath: "time", - }, - { - name: "external package with full path", - input: "github.com/google/uuid.UUID", - wantGoType: "uuid.UUID", - wantImportPath: "github.com/google/uuid", - }, - { - name: "external package with nested path", - input: "github.com/shopspring/decimal.Decimal", - wantGoType: "decimal.Decimal", - wantImportPath: "github.com/shopspring/decimal", - }, - { - name: "json.RawMessage", - input: "json.RawMessage", - wantGoType: "json.RawMessage", - wantImportPath: "json", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := parseTypeMapping(tt.input) - assert.Equal(t, tt.wantGoType, got.GoType) - assert.Equal(t, tt.wantImportPath, got.ImportPath) - }) - } -} - -func TestExtractGoTypeAnnotation(t *testing.T) { - tests := []struct { - name string - schema *config.Schema - want string - }{ - { - name: "nil schema", - schema: nil, - want: "", - }, - { - name: "schema without extra", - schema: &config.Schema{ - Type: "string", - }, - want: "", - }, - { - name: "schema with go.probo.inc/mcpgen/type annotation", - schema: &config.Schema{ - Type: "string", - Extra: map[string]any{ - "go.probo.inc/mcpgen/type": "time.Time", - }, - }, - want: "time.Time", - }, - { - name: "schema with other annotations", - schema: &config.Schema{ - Type: "string", - Extra: map[string]any{ - "x-custom": "value", - }, - }, - want: "", - }, - { - name: "schema with non-string go.probo.inc/mcpgen/type", - schema: &config.Schema{ - Type: "string", - Extra: map[string]any{ - "go.probo.inc/mcpgen/type": 123, - }, - }, - want: "", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := extractGoTypeAnnotation(tt.schema) - assert.Equal(t, tt.want, got) - }) - } -} - -func TestTypeGeneratorCustomMappings(t *testing.T) { - tests := []struct { - name string - setupGen func(*TypeGenerator) - schema *config.Schema - hint string - wantType string - wantImports []string - wantErr bool - }{ - { - name: "custom mapping for ref", - setupGen: func(g *TypeGenerator) { - g.AddCustomMapping("Timestamp", &CustomTypeMapping{ - GoType: "time.Time", - ImportPath: "time", - }) - }, - schema: &config.Schema{ - Ref: "#/components/schemas/Timestamp", - }, - hint: "CreatedAt", - wantType: "time.Time", - wantImports: []string{"time"}, - wantErr: false, - }, - { - name: "custom mapping for UUID", - setupGen: func(g *TypeGenerator) { - g.AddCustomMapping("UUID", &CustomTypeMapping{ - GoType: "uuid.UUID", - ImportPath: "github.com/google/uuid", - }) - }, - schema: &config.Schema{ - Ref: "#/components/schemas/UUID", - }, - hint: "ID", - wantType: "uuid.UUID", - wantImports: []string{"github.com/google/uuid"}, - wantErr: false, - }, - { - name: "nullable custom type", - setupGen: func(g *TypeGenerator) { - g.AddCustomMapping("Timestamp", &CustomTypeMapping{ - GoType: "time.Time", - ImportPath: "time", - }) - }, - schema: &config.Schema{ - AnyOf: []*config.Schema{ - {Ref: "#/components/schemas/Timestamp"}, - {Type: "null"}, - }, - }, - hint: "UpdatedAt", - wantType: "*time.Time", - wantImports: []string{"time"}, - wantErr: false, - }, - { - name: "regular ref without custom mapping", - setupGen: func(g *TypeGenerator) { - // No custom mapping added - }, - schema: &config.Schema{ - Ref: "#/components/schemas/User", - }, - hint: "Owner", - wantType: "*User", - wantImports: []string{}, - wantErr: false, - }, - { - name: "string type", - setupGen: func(g *TypeGenerator) { - // No custom mapping needed - }, - schema: &config.Schema{ - Type: "string", - }, - hint: "Name", - wantType: "string", - wantImports: []string{}, - wantErr: false, - }, - { - name: "time.Time from format", - setupGen: func(g *TypeGenerator) { - // No custom mapping, should use format - }, - schema: &config.Schema{ - Type: "string", - Format: "date-time", - }, - hint: "CreatedAt", - wantType: "time.Time", - wantImports: []string{"time"}, - wantErr: false, - }, - { - name: "nullable time.Time from format with types array", - setupGen: func(g *TypeGenerator) { - // No custom mapping, should use format - }, - schema: &config.Schema{ - Types: []string{"string", "null"}, - Format: "date-time", - }, - hint: "ContractStartDate", - wantType: "*time.Time", - wantImports: []string{"time"}, - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gen := NewTypeGenerator() - tt.setupGen(gen) - - gotType, err := gen.goType(tt.schema, tt.hint) - if tt.wantErr { - assert.Error(t, err) - return - } - require.NoError(t, err) - - assert.Equal(t, tt.wantType, gotType) - - for _, wantImport := range tt.wantImports { - assert.True(t, gen.imports[wantImport], "expected import %q not found", wantImport) - } - }) - } -} - -func TestTypeGeneratorSkipsCustomMappedSchemas(t *testing.T) { - gen := NewTypeGenerator() - - gen.AddCustomMapping("Timestamp", &CustomTypeMapping{ - GoType: "time.Time", - ImportPath: "time", - }) - - gen.AddSchema("Timestamp", &config.Schema{ - Type: "string", - Format: "date-time", - }) - - // Add a schema that references Timestamp (so the import gets added) - gen.AddSchema("Event", &config.Schema{ - Type: "object", - Properties: map[string]*config.Schema{ - "name": {Type: "string"}, - "createdAt": { - Ref: "#/components/schemas/Timestamp", - }, - }, - }) - - gen.AddSchema("User", &config.Schema{ - Type: "object", - Properties: map[string]*config.Schema{ - "name": {Type: "string"}, - }, - }) - - code, err := gen.Generate("test") - if err != nil { - t.Fatalf("Generate() error = %v", err) - } - - codeStr := string(code) - - if contains := containsTypeDefinition(codeStr, "type Timestamp"); contains { - t.Error("Generated code should not contain Timestamp type definition (it's custom mapped)") - } - - if !containsTypeDefinition(codeStr, "type User struct") { - t.Error("Generated code should contain User type definition") - } - - if !containsTypeDefinition(codeStr, "type Event struct") { - t.Error("Generated code should contain Event type definition") - } - - if !containsImport(codeStr, "time") { - t.Error("Generated code should import time package") - } -} - -func TestIsNullableType(t *testing.T) { - tests := []struct { - name string - schema *config.Schema - wantNullable bool - wantSchema *config.Schema - }{ - { - name: "anyOf with null and type", - schema: &config.Schema{ - AnyOf: []*config.Schema{ - {Type: "string"}, - {Type: "null"}, - }, - }, - wantNullable: true, - wantSchema: &config.Schema{Type: "string"}, - }, - { - name: "anyOf with null and ref", - schema: &config.Schema{ - AnyOf: []*config.Schema{ - {Ref: "#/components/schemas/User"}, - {Type: "null"}, - }, - }, - wantNullable: true, - wantSchema: &config.Schema{Ref: "#/components/schemas/User"}, - }, - { - name: "anyOf with multiple types (not nullable)", - schema: &config.Schema{ - AnyOf: []*config.Schema{ - {Type: "string"}, - {Type: "number"}, - }, - }, - wantNullable: false, - wantSchema: nil, - }, - { - name: "regular type (not nullable)", - schema: &config.Schema{ - Type: "string", - }, - wantNullable: false, - wantSchema: nil, - }, - { - name: "types array with null", - schema: &config.Schema{ - Types: []string{"string", "null"}, - }, - wantNullable: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotNullable, gotSchema := isNullableType(tt.schema) - if gotNullable != tt.wantNullable { - t.Errorf("isNullableType() nullable = %v, want %v", gotNullable, tt.wantNullable) - } - if tt.wantSchema != nil && gotSchema != nil { - if gotSchema.Type != tt.wantSchema.Type || gotSchema.Ref != tt.wantSchema.Ref { - t.Errorf("isNullableType() schema = %+v, want %+v", gotSchema, tt.wantSchema) - } - } - }) - } -} - -func TestEnumGeneration(t *testing.T) { - gen := NewTypeGenerator() - - enumSchema := &config.Schema{ - Type: "string", - Enum: []any{"pending", "in_progress", "completed"}, - Description: "Task status", - } - - code, err := gen.generateEnum("Status", enumSchema) - if err != nil { - t.Fatalf("generateEnum() error = %v", err) - } - - if !containsTypeDefinition(code, "type Status string") { - t.Error("Generated enum should contain type definition") - } - - expectedConstants := []string{"StatusPending", "StatusInProgress", "StatusCompleted"} - for _, constant := range expectedConstants { - if !containsString(code, constant) { - t.Errorf("Generated enum should contain constant %q", constant) - } - } - - if !containsString(code, "func (e Status) IsValid() bool") { - t.Error("Generated enum should contain IsValid method") - } - - if !containsString(code, "func (e *Status) UnmarshalJSON") { - t.Error("Generated enum should contain UnmarshalJSON method") - } - if !containsString(code, "func (e Status) MarshalJSON") { - t.Error("Generated enum should contain MarshalJSON method") - } -} - -func containsTypeDefinition(code, typeDef string) bool { - return containsString(code, typeDef) -} - -func containsImport(code, importPath string) bool { - return containsString(code, `"`+importPath+`"`) -} - -func containsString(haystack, needle string) bool { - // Use jsonschema import to avoid unused import error - _ = jsonschema.Schema{} - // Simple contains check - for i := 0; i <= len(haystack)-len(needle); i++ { - if haystack[i:i+len(needle)] == needle { - return true - } - } - return false -} - -func TestGenerateArrayType(t *testing.T) { - tests := []struct { - name string - schema *config.Schema - typeName string - depth int - want string - wantContains []string - wantErr bool - }{ - { - name: "array of strings (top-level)", - schema: &config.Schema{ - Type: "array", - Description: "An array of tags", - Items: &config.Schema{ - Type: "string", - }, - }, - typeName: "Tags", - depth: 0, - wantContains: []string{"type Tags []string", "An array of tags"}, - wantErr: false, - }, - { - name: "array of strings (nested)", - schema: &config.Schema{ - Type: "array", - Items: &config.Schema{ - Type: "string", - }, - }, - typeName: "Tags", - depth: 1, - want: "[]string", - wantErr: false, - }, - { - name: "array of numbers (top-level)", - schema: &config.Schema{ - Type: "array", - Items: &config.Schema{ - Type: "number", - }, - }, - typeName: "Scores", - depth: 0, - wantContains: []string{"type Scores []float64"}, - wantErr: false, - }, - { - name: "array of numbers (nested)", - schema: &config.Schema{ - Type: "array", - Items: &config.Schema{ - Type: "number", - }, - }, - typeName: "Scores", - depth: 1, - want: "[]float64", - wantErr: false, - }, - { - name: "array of objects (ref, top-level)", - schema: &config.Schema{ - Type: "array", - Items: &config.Schema{ - Ref: "#/components/schemas/User", - }, - }, - typeName: "Users", - depth: 0, - wantContains: []string{"type Users []*User"}, - wantErr: false, - }, - { - name: "array of objects (ref, nested)", - schema: &config.Schema{ - Type: "array", - Items: &config.Schema{ - Ref: "#/components/schemas/User", - }, - }, - typeName: "Users", - depth: 1, - want: "[]*User", - wantErr: false, - }, - { - name: "array without items (top-level)", - schema: &config.Schema{ - Type: "array", - }, - typeName: "Unknown", - depth: 0, - wantContains: []string{"type Unknown []any"}, - wantErr: false, - }, - { - name: "array without items (nested)", - schema: &config.Schema{ - Type: "array", - }, - typeName: "Unknown", - depth: 1, - want: "[]any", - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gen := NewTypeGenerator() - got, err := gen.generateArrayType(tt.typeName, tt.schema, tt.depth) - if tt.wantErr { - assert.Error(t, err) - return - } - require.NoError(t, err) - - if tt.want != "" { - // Exact match test - if got != tt.want { - t.Errorf("generateArrayType() = %q, want %q", got, tt.want) - } - } else { - // Contains test - for _, want := range tt.wantContains { - if !containsString(got, want) { - t.Errorf("generateArrayType() should contain %q\nGot: %s", want, got) - } - } - } - }) - } -} - -func TestGeneratePrimitiveTypeAlias(t *testing.T) { - tests := []struct { - name string - typeName string - schema *config.Schema - goType string - wantContains []string - }{ - { - name: "string with description", - typeName: "Username", - schema: &config.Schema{ - Description: "A username string", - }, - goType: "string", - wantContains: []string{"type Username string", "A username string"}, - }, - { - name: "integer without description", - typeName: "Count", - schema: &config.Schema{}, - goType: "int", - wantContains: []string{"type Count int", "Count represents a int schema"}, - }, - { - name: "float64 type", - typeName: "Score", - schema: &config.Schema{}, - goType: "float64", - wantContains: []string{"type Score float64"}, - }, - { - name: "boolean type", - typeName: "IsActive", - schema: &config.Schema{}, - goType: "bool", - wantContains: []string{"type IsActive bool"}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gen := NewTypeGenerator() - got, err := gen.generatePrimitiveTypeAlias(tt.typeName, tt.schema, tt.goType) - if err != nil { - t.Errorf("generatePrimitiveTypeAlias() error = %v", err) - return - } - - for _, want := range tt.wantContains { - if !containsString(got, want) { - t.Errorf("generatePrimitiveTypeAlias() should contain %q\nGot: %s", want, got) - } - } - }) - } -} - -func TestGoTypeEdgeCases(t *testing.T) { - tests := []struct { - name string - setupGen func(*TypeGenerator) - schema *config.Schema - hint string - want string - wantErr bool - }{ - { - name: "object with title", - setupGen: func(g *TypeGenerator) { - }, - schema: &config.Schema{ - Type: "object", - Title: "CustomObject", - Properties: map[string]*config.Schema{ - "name": {Type: "string"}, - }, - }, - hint: "Field", - want: "CustomObject", - wantErr: false, - }, - { - name: "object with properties but no title", - setupGen: func(g *TypeGenerator) { - }, - schema: &config.Schema{ - Type: "object", - Properties: map[string]*config.Schema{ - "snapshot_id": {Type: "string"}, - }, - }, - hint: "Filter", - want: "Filter", - wantErr: false, - }, - { - name: "object without title or properties", - setupGen: func(g *TypeGenerator) { - }, - schema: &config.Schema{ - Type: "object", - }, - hint: "Metadata", - want: "map[string]any", - wantErr: false, - }, - { - name: "null type", - setupGen: func(g *TypeGenerator) { - }, - schema: &config.Schema{ - Type: "null", - }, - hint: "Value", - want: "any", - wantErr: false, - }, - { - name: "schema with only properties (no type)", - setupGen: func(g *TypeGenerator) { - }, - schema: &config.Schema{ - Properties: map[string]*config.Schema{ - "id": {Type: "string"}, - "name": {Type: "string"}, - }, - }, - hint: "User", - want: "User", - wantErr: false, - }, - { - name: "integer type", - setupGen: func(g *TypeGenerator) { - }, - schema: &config.Schema{ - Type: "integer", - }, - hint: "Count", - want: "int", - wantErr: false, - }, - { - name: "number type", - setupGen: func(g *TypeGenerator) { - }, - schema: &config.Schema{ - Type: "number", - }, - hint: "Price", - want: "float64", - wantErr: false, - }, - { - name: "boolean type", - setupGen: func(g *TypeGenerator) { - }, - schema: &config.Schema{ - Type: "boolean", - }, - hint: "Active", - want: "bool", - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gen := NewTypeGenerator() - tt.setupGen(gen) - - got, err := gen.goType(tt.schema, tt.hint) - if tt.wantErr { - assert.Error(t, err) - return - } - require.NoError(t, err) - assert.Equal(t, tt.want, got) - }) - } -} - -func TestGoStringType(t *testing.T) { - tests := []struct { - name string - schema *config.Schema - want string - }{ - { - name: "date-time format", - schema: &config.Schema{Format: "date-time"}, - want: "time.Time", - }, - { - name: "date format", - schema: &config.Schema{Format: "date"}, - want: "string", - }, - { - name: "email format", - schema: &config.Schema{Format: "email"}, - want: "string", - }, - { - name: "uuid format", - schema: &config.Schema{Format: "uuid"}, - want: "string", - }, - { - name: "uri format", - schema: &config.Schema{Format: "uri"}, - want: "string", - }, - { - name: "no format", - schema: &config.Schema{}, - want: "string", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gen := NewTypeGenerator() - got := gen.goStringType(tt.schema) - assert.Equal(t, tt.want, got) - - if tt.want == "time.Time" && !gen.imports["time"] { - t.Error("time.Time should add time import") - } - }) - } -} - -func TestGenerateStruct(t *testing.T) { - tests := []struct { - name string - schema *config.Schema - want []string // Strings that should be in the output - wantErr bool - }{ - { - name: "struct with description", - schema: &config.Schema{ - Description: "A user object", - Type: "object", - Properties: map[string]*config.Schema{ - "name": { - Type: "string", - Description: "User name", - }, - "age": { - Type: "integer", - }, - }, - Required: []string{"name"}, - }, - want: []string{ - "// A user object", - "type User struct", - "Name string", - "Age *int", // Age is not required, so it's a pointer - "`json:\"name\"`", - "`json:\"age,omitempty\"`", - }, - wantErr: false, - }, - { - name: "struct with title", - schema: &config.Schema{ - Title: "Person", - Type: "object", - Properties: map[string]*config.Schema{ - "id": {Type: "string"}, - }, - }, - want: []string{ - "// Person", - "type Person struct", - }, - wantErr: false, - }, - { - name: "struct with no description or title", - schema: &config.Schema{ - Type: "object", - Properties: map[string]*config.Schema{ - "value": {Type: "string"}, - }, - }, - want: []string{ - "// Anonymous represents the schema", - "type Anonymous struct", - }, - wantErr: false, - }, - { - name: "struct with omittable on non-nullable field - should error", - schema: &config.Schema{ - Type: "object", - Properties: map[string]*config.Schema{ - "name": { - Type: "string", - Extra: map[string]any{ - "go.probo.inc/mcpgen/omittable": true, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "struct with omittable on nullable field - should succeed", - schema: &config.Schema{ - Type: "object", - Properties: map[string]*config.Schema{ - "description": { - AnyOf: []*config.Schema{ - {Type: "string"}, - {Type: "null"}, - }, - Extra: map[string]any{ - "go.probo.inc/mcpgen/omittable": true, - }, - }, - }, - }, - want: []string{ - "type UpdateInput struct", - "Description mcp.Omittable[*string]", - }, - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gen := NewTypeGenerator() - typeName := "User" - if tt.schema.Title != "" { - typeName = tt.schema.Title - } else if tt.name == "struct with no description or title" { - typeName = "Anonymous" - } else if tt.name == "struct with omittable on nullable field - should succeed" { - typeName = "UpdateInput" - } - - got, err := gen.generateStruct(typeName, tt.schema, 0) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - for _, wantStr := range tt.want { - assert.Contains(t, got, wantStr) - } - }) - } -} - -func TestToGoTypeName(t *testing.T) { - tests := []struct { - input string - want string - }{ - {"user", "User"}, - {"user_profile", "UserProfile"}, - {"user-settings", "UserSettings"}, - {"user.data", "UserData"}, - {"user_input.json", "User"}, // .json is stripped first, then _input - {"task_input_schema", "TaskInput"}, // only _schema is stripped as a suffix - {"task_schema", "Task"}, // _schema is stripped - {"my-cool-type", "MyCoolType"}, - {"id", "Id"}, - {"url", "Url"}, - } - - for _, tt := range tests { - t.Run(tt.input, func(t *testing.T) { - got := toGoTypeName(tt.input) - if got != tt.want { - t.Errorf("toGoTypeName(%q) = %q, want %q", tt.input, got, tt.want) - } - }) - } -} - -func TestToGoFieldName(t *testing.T) { - tests := []struct { - input string - want string - }{ - {"id", "ID"}, - {"url", "URL"}, - {"uri", "URI"}, - {"api", "API"}, - {"json", "JSON"}, - {"xml", "XML"}, - {"html", "HTML"}, - {"http", "HTTP"}, - {"https", "HTTPS"}, - {"sql", "SQL"}, - {"ssh", "SSH"}, - {"tcp", "TCP"}, - {"udp", "UDP"}, - {"ip", "IP"}, - {"ui", "UI"}, - {"uuid", "UUID"}, - {"jwt", "JWT"}, - {"oauth", "OAuth"}, - {"snapshot_id", "SnapshotID"}, - {"user_id", "UserID"}, - {"account_id", "AccountID"}, - {"resource_url", "ResourceURL"}, - {"redirect_uri", "RedirectURI"}, - {"object_uuid", "ObjectUUID"}, - {"session_jwt", "SessionJWT"}, - {"api_key", "APIKey"}, - {"api_secret", "APISecret"}, - {"http_status", "HTTPStatus"}, - {"https_enabled", "HTTPSEnabled"}, - {"json_data", "JSONData"}, - {"xml_content", "XMLContent"}, - {"html_body", "HTMLBody"}, - {"sql_query", "SQLQuery"}, - {"ip_address", "IPAddress"}, - {"ui_state", "UIState"}, - {"user_api_key", "UserAPIKey"}, - {"get_json_data", "GetJSONData"}, - {"parse_xml_content", "ParseXMLContent"}, - {"api_url", "APIURL"}, - {"http_api_key", "HTTPAPIKey"}, - {"json_api_url", "JSONAPIURL"}, - {"user_name", "UserName"}, - {"first-name", "FirstName"}, - {"created_at", "CreatedAt"}, - {"is_active", "IsActive"}, - } - - for _, tt := range tests { - t.Run(tt.input, func(t *testing.T) { - got := toGoFieldName(tt.input) - if got != tt.want { - t.Errorf("toGoFieldName(%q) = %q, want %q", tt.input, got, tt.want) - } - }) - } -} - -func TestToEnumConstName(t *testing.T) { - tests := []struct { - enumType string - value string - want string - }{ - {"Status", "pending", "StatusPending"}, - {"Status", "in_progress", "StatusInProgress"}, - {"ColorType", "red", "ColorRed"}, - {"Priority", "high-priority", "PriorityHighPriority"}, - } - - for _, tt := range tests { - t.Run(tt.enumType+"_"+tt.value, func(t *testing.T) { - got := toEnumConstName(tt.enumType, tt.value) - if got != tt.want { - t.Errorf("toEnumConstName(%q, %q) = %q, want %q", tt.enumType, tt.value, got, tt.want) - } - }) - } -} diff --git a/third_party/mcpgen/internal/config/config.go b/third_party/mcpgen/internal/config/config.go deleted file mode 100644 index 57064f428..000000000 --- a/third_party/mcpgen/internal/config/config.go +++ /dev/null @@ -1,208 +0,0 @@ -package config - -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - - "github.com/google/jsonschema-go/jsonschema" - "gopkg.in/yaml.v3" -) - -type Config struct { - Spec string `yaml:"spec" json:"spec"` - Output string `yaml:"output" json:"output"` - Exec ExecConfig `yaml:"exec,omitempty" json:"exec,omitempty"` - Resolver ResolverConfig `yaml:"resolver" json:"resolver"` - Model ModelConfig `yaml:"model,omitempty" json:"model,omitempty"` - Models ModelsConfig `yaml:"models,omitempty" json:"models,omitempty"` -} - -type ExecConfig struct { - Package string `yaml:"package,omitempty" json:"package,omitempty"` - Filename string `yaml:"filename,omitempty" json:"filename,omitempty"` -} - -type ResolverConfig struct { - Package string `yaml:"package" json:"package"` - Filename string `yaml:"filename" json:"filename"` - Type string `yaml:"type" json:"type"` - Preserve bool `yaml:"preserve" json:"preserve"` -} - -type ModelConfig struct { - Package string `yaml:"package,omitempty" json:"package,omitempty"` - Filename string `yaml:"filename,omitempty" json:"filename,omitempty"` -} - -type ModelsConfig struct { - // Map schema names to custom Go types - // Example: User: github.com/myorg/models.User - Models map[string]TypeMapping `yaml:",inline,omitempty" json:",inline,omitempty"` -} - -type TypeMapping struct { - // Model is the fully qualified Go type to use - // Example: github.com/google/uuid.UUID - Model string `yaml:"model" json:"model"` -} - -type ServerInfo struct { - Title string `yaml:"title" json:"title"` - Version string `yaml:"version" json:"version"` - Description string `yaml:"description,omitempty" json:"description,omitempty"` -} - -type Components struct { - Schemas map[string]*jsonschema.Schema `yaml:"schemas,omitempty" json:"schemas,omitempty"` -} - -type Schema = jsonschema.Schema - -type ToolHints struct { - Readonly bool `yaml:"readonly,omitempty" json:"readonly,omitempty"` - Destructive bool `yaml:"destructive,omitempty" json:"destructive,omitempty"` - Idempotent bool `yaml:"idempotent,omitempty" json:"idempotent,omitempty"` - OpenWorld bool `yaml:"openWorld,omitempty" json:"openWorld,omitempty"` -} - -type Tool struct { - Name string `yaml:"name" json:"name"` - Title string `yaml:"title,omitempty" json:"title,omitempty"` - Description string `yaml:"description,omitempty" json:"description,omitempty"` - InputSchema *Schema `yaml:"inputSchema" json:"inputSchema"` - OutputSchema *Schema `yaml:"outputSchema,omitempty" json:"outputSchema,omitempty"` - Hints *ToolHints `yaml:"hints,omitempty" json:"hints,omitempty"` - Annotations map[string]string `yaml:"annotations,omitempty" json:"annotations,omitempty"` - Handler string `yaml:"handler,omitempty" json:"handler,omitempty"` -} - -type Resource struct { - URI string `yaml:"uri,omitempty" json:"uri,omitempty"` - Name string `yaml:"name" json:"name"` - Description string `yaml:"description,omitempty" json:"description,omitempty"` - MimeType string `yaml:"mimeType,omitempty" json:"mimeType,omitempty"` - URITemplate string `yaml:"uriTemplate,omitempty" json:"uriTemplate,omitempty"` - Schema *Schema `yaml:"schema,omitempty" json:"schema,omitempty"` - Readonly bool `yaml:"readonly,omitempty" json:"readonly,omitempty"` - Annotations map[string]string `yaml:"annotations,omitempty" json:"annotations,omitempty"` - Handler string `yaml:"handler,omitempty" json:"handler,omitempty"` -} - -type Prompt struct { - Name string `yaml:"name" json:"name"` - Description string `yaml:"description,omitempty" json:"description,omitempty"` - Arguments []PromptArgument `yaml:"arguments,omitempty" json:"arguments,omitempty"` - Annotations map[string]string `yaml:"annotations,omitempty" json:"annotations,omitempty"` - Handler string `yaml:"handler,omitempty" json:"handler,omitempty"` -} - -type PromptArgument struct { - Name string `yaml:"name" json:"name"` - Description string `yaml:"description,omitempty" json:"description,omitempty"` - Required bool `yaml:"required,omitempty" json:"required,omitempty"` -} - -func Load(path string) (*Config, *MCPSpec, error) { - data, err := os.ReadFile(path) - if err != nil { - return nil, nil, fmt.Errorf("failed to read config file: %w", err) - } - - config := &Config{ - Spec: "schema.yaml", - Output: "generated", - Exec: ExecConfig{ - Package: "server", - Filename: "server/server.go", - }, - Resolver: ResolverConfig{ - Package: "generated", - Filename: "resolver.go", - Type: "Resolver", - Preserve: true, - }, - Model: ModelConfig{ - Package: "generated", - Filename: "models.go", - }, - } - - ext := filepath.Ext(path) - switch ext { - case ".yaml", ".yml": - if err := yaml.Unmarshal(data, config); err != nil { - return nil, nil, fmt.Errorf("failed to parse YAML config: %w", err) - } - case ".json": - if err := json.Unmarshal(data, config); err != nil { - return nil, nil, fmt.Errorf("failed to parse JSON config: %w", err) - } - default: - return nil, nil, fmt.Errorf("unsupported config file format: %s (use .yaml, .yml, or .json)", ext) - } - - if err := config.Validate(); err != nil { - return nil, nil, fmt.Errorf("invalid configuration: %w", err) - } - - // Make output path absolute relative to config file directory - configDir := filepath.Dir(path) - if !filepath.IsAbs(config.Output) { - config.Output = filepath.Join(configDir, config.Output) - } - - specPath := config.Spec - if !filepath.IsAbs(specPath) { - configDir := filepath.Dir(path) - specPath = filepath.Join(configDir, specPath) - } - - if _, err := os.Stat(specPath); os.IsNotExist(err) { - basePath := specPath - for _, ext := range []string{".yaml", ".yml", ".json"} { - tryPath := basePath - if filepath.Ext(tryPath) == "" { - tryPath = basePath + ext - } else { - tryPath = basePath[:len(basePath)-len(filepath.Ext(basePath))] + ext - } - if _, err := os.Stat(tryPath); err == nil { - specPath = tryPath - break - } - } - } - - spec, err := LoadMCPSpec(specPath) - if err != nil { - return nil, nil, fmt.Errorf("failed to load MCP spec from %s: %w", specPath, err) - } - - return config, spec, nil -} - -func (c *Config) Validate() error { - if c.Spec == "" { - return fmt.Errorf("spec path is required") - } - if c.Output == "" { - return fmt.Errorf("output is required") - } - if c.Exec.Package == "" { - return fmt.Errorf("exec.package is required") - } - if c.Resolver.Package == "" { - return fmt.Errorf("resolver.package is required") - } - if c.Model.Package == "" { - return fmt.Errorf("model.package is required") - } - - return nil -} - -func IsSchemaRef(s *Schema) bool { - return s != nil && s.Ref != "" -} diff --git a/third_party/mcpgen/internal/config/spec.go b/third_party/mcpgen/internal/config/spec.go deleted file mode 100644 index c949d465f..000000000 --- a/third_party/mcpgen/internal/config/spec.go +++ /dev/null @@ -1,114 +0,0 @@ -package config - -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - - "gopkg.in/yaml.v3" -) - -type MCPSpec struct { - Info ServerInfo `yaml:"info" json:"info"` - Components Components `yaml:"components,omitempty" json:"components,omitempty"` - Tools []Tool `yaml:"tools,omitempty" json:"tools,omitempty"` - Resources []Resource `yaml:"resources,omitempty" json:"resources,omitempty"` - Prompts []Prompt `yaml:"prompts,omitempty" json:"prompts,omitempty"` -} - -func LoadMCPSpec(path string) (*MCPSpec, error) { - data, err := os.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("failed to read MCP spec file: %w", err) - } - - spec := &MCPSpec{} - - ext := filepath.Ext(path) - switch ext { - case ".yaml", ".yml": - var intermediate interface{} - if err := yaml.Unmarshal(data, &intermediate); err != nil { - return nil, fmt.Errorf("failed to parse YAML spec: %w", err) - } - jsonData, err := json.Marshal(intermediate) - if err != nil { - return nil, fmt.Errorf("failed to convert YAML to JSON: %w", err) - } - if err := json.Unmarshal(jsonData, spec); err != nil { - return nil, fmt.Errorf("failed to unmarshal spec: %w", err) - } - case ".json": - if err := json.Unmarshal(data, spec); err != nil { - return nil, fmt.Errorf("failed to parse JSON spec: %w", err) - } - default: - return nil, fmt.Errorf("unsupported spec file format: %s (use .yaml, .yml, or .json)", ext) - } - - if err := spec.Validate(); err != nil { - return nil, fmt.Errorf("invalid MCP specification: %w", err) - } - - return spec, nil -} - -func (s *MCPSpec) Validate() error { - if s.Info.Title == "" { - return fmt.Errorf("info.title is required") - } - if s.Info.Version == "" { - return fmt.Errorf("info.version is required") - } - - for i, tool := range s.Tools { - if tool.Name == "" { - return fmt.Errorf("tools[%d].name is required", i) - } - if tool.InputSchema == nil { - return fmt.Errorf("tools[%d].inputSchema is required", i) - } - } - - for i, resource := range s.Resources { - if resource.Name == "" { - return fmt.Errorf("resources[%d].name is required", i) - } - if resource.URI == "" && resource.URITemplate == "" { - return fmt.Errorf("resources[%d] must have either uri or uriTemplate", i) - } - if resource.URI != "" && resource.URITemplate != "" { - return fmt.Errorf("resources[%d] cannot have both uri and uriTemplate", i) - } - } - - for i, prompt := range s.Prompts { - if prompt.Name == "" { - return fmt.Errorf("prompts[%d].name is required", i) - } - } - - return nil -} - -func (s *MCPSpec) ResolveSchemaRef(ref string) (*Schema, error) { - if len(ref) > 0 && ref[0] == '#' { - if ref == "#/components/schemas" { - return nil, fmt.Errorf("incomplete schema reference: %s", ref) - } - - const prefix = "#/components/schemas/" - if len(ref) > len(prefix) && ref[:len(prefix)] == prefix { - schemaName := ref[len(prefix):] - if schema, ok := s.Components.Schemas[schemaName]; ok { - return schema, nil - } - return nil, fmt.Errorf("schema not found: %s", schemaName) - } - - return nil, fmt.Errorf("unsupported reference format: %s", ref) - } - - return nil, nil -} diff --git a/third_party/mcpgen/internal/schema/schema.go b/third_party/mcpgen/internal/schema/schema.go deleted file mode 100644 index a185941aa..000000000 --- a/third_party/mcpgen/internal/schema/schema.go +++ /dev/null @@ -1,85 +0,0 @@ -package schema - -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - - "github.com/google/jsonschema-go/jsonschema" -) - -type Schema = jsonschema.Schema - -type Loader struct { - schemas map[string]*Schema - baseDir string -} - -func NewLoader(baseDir string) *Loader { - return &Loader{ - schemas: make(map[string]*Schema), - baseDir: baseDir, - } -} - -func (l *Loader) Load(path string) (*Schema, error) { - absPath, err := filepath.Abs(path) - if err != nil { - return nil, fmt.Errorf("failed to get absolute path: %w", err) - } - - if schema, ok := l.schemas[absPath]; ok { - return schema, nil - } - - data, err := os.ReadFile(absPath) - if err != nil { - return nil, fmt.Errorf("failed to read schema file %s: %w", path, err) - } - - var schema Schema - if err := json.Unmarshal(data, &schema); err != nil { - return nil, fmt.Errorf("failed to parse schema file %s: %w", path, err) - } - - l.schemas[absPath] = &schema - - return &schema, nil -} - -func GetType(s *Schema) string { - if s.Type != "" { - return s.Type - } - if len(s.Types) > 0 { - return s.Types[0] - } - return "" -} - -func IsRequired(s *Schema, propName string) bool { - for _, req := range s.Required { - if req == propName { - return true - } - } - return false -} - -// IsOmittable checks if a schema property has the go.probo.inc/mcpgen/omittable annotation set to true. -// This is used to wrap fields in mcp.Omittable[T] to distinguish between -// "not set", "set to null", and "set to value". -func IsOmittable(s *Schema) bool { - if s == nil || s.Extra == nil { - return false - } - - if omittable, ok := s.Extra["go.probo.inc/mcpgen/omittable"]; ok { - if omittableBool, ok := omittable.(bool); ok { - return omittableBool - } - } - - return false -} diff --git a/third_party/mcpgen/main.go b/third_party/mcpgen/main.go deleted file mode 100644 index 9c31f3acf..000000000 --- a/third_party/mcpgen/main.go +++ /dev/null @@ -1,185 +0,0 @@ -package main - -import ( - "fmt" - "os" - "path/filepath" - - "github.com/spf13/cobra" - "go.probo.inc/mcpgen/internal/codegen" - "go.probo.inc/mcpgen/internal/config" -) - -var version = "dev" - -func main() { - if err := rootCmd.Execute(); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } -} - -var rootCmd = &cobra.Command{ - Use: "mcpgen", - Short: "A code generator for Model Context Protocol (MCP) servers", - Long: `mcpgen is a gqlgen-like code generator for building MCP servers in Go. -It generates type-safe Go code from JSON Schema definitions for tools, resources, and prompts.`, -} - -var versionCmd = &cobra.Command{ - Use: "version", - Short: "Print the version number of mcpgen", - Run: func(cmd *cobra.Command, args []string) { - fmt.Printf("mcpgen %s\n", version) - }, -} - -var generateCmd = &cobra.Command{ - Use: "generate", - Short: "Generate Go code from mcpgen configuration", - Long: `Reads mcpgen.yaml (or mcpgen.yml) configuration file and generates: - - Type-safe Go structs from JSON Schemas - - MCP server boilerplate code - - Handler function stubs for tools, resources, and prompts`, - RunE: func(cmd *cobra.Command, args []string) error { - configFile, _ := cmd.Flags().GetString("config") - return runGenerate(configFile) - }, -} - -var initCmd = &cobra.Command{ - Use: "init [name]", - Short: "Initialize a new MCP server project", - Long: `Creates a new MCP server project with example configuration and file structure.`, - Args: cobra.MaximumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - name := "my-mcp-server" - if len(args) > 0 { - name = args[0] - } - return runInit(name) - }, -} - -func init() { - generateCmd.Flags().StringP("config", "c", "mcpgen.yaml", "Path to config file") - - rootCmd.AddCommand(versionCmd) - rootCmd.AddCommand(generateCmd) - rootCmd.AddCommand(initCmd) -} - -func runGenerate(configFile string) error { - if _, err := os.Stat(configFile); os.IsNotExist(err) { - if configFile == "mcpgen.yaml" { - if _, err := os.Stat("mcpgen.yml"); err == nil { - configFile = "mcpgen.yml" - } - } - } - - fmt.Printf("Loading configuration from %s...\n", configFile) - - cfg, spec, err := config.Load(configFile) - if err != nil { - return fmt.Errorf("failed to load configuration: %w", err) - } - - fmt.Printf("Generating code for %s v%s...\n", spec.Info.Title, spec.Info.Version) - - gen := codegen.New(cfg, spec) - - if err := gen.Generate(); err != nil { - return fmt.Errorf("code generation failed: %w", err) - } - - fmt.Println("āœ“ Code generation completed successfully!") - return nil -} - -func runInit(name string) error { - fmt.Printf("Initializing new MCP server project: %s\n", name) - - if err := os.MkdirAll(name, 0755); err != nil { - return fmt.Errorf("failed to create project directory: %w", err) - } - - configContent := `# mcpgen configuration -# Path to MCP API specification -spec: schema.yaml - -# Output directory for generated code -output: generated - -# Resolver configuration -resolver: - package: generated - filename: resolver.go - type: Resolver - preserve: true - -# Model configuration -model: - package: generated - filename: models.go -` - - configPath := filepath.Join(name, "mcpgen.yaml") - if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { - return fmt.Errorf("failed to write config file: %w", err) - } - - schemaContent := fmt.Sprintf(`# MCP API Specification -# This file contains the pure MCP API definition - -info: - title: %s - version: 1.0.0 - description: An example MCP server - -# Reusable schema components -components: - schemas: - ExampleInput: - type: object - properties: - message: - type: string - description: The message to process - required: [message] - -# MCP Tools -tools: - - name: example_tool - title: Example Tool - description: An example tool that processes messages - hints: - readonly: false - destructive: false - idempotent: true - inputSchema: - $ref: "#/components/schemas/ExampleInput" - -# MCP Resources -resources: [] - -# MCP Prompts -prompts: [] -`, name) - - schemaPath := filepath.Join(name, "schema.yaml") - if err := os.WriteFile(schemaPath, []byte(schemaContent), 0644); err != nil { - return fmt.Errorf("failed to write schema file: %w", err) - } - - fmt.Printf("\nāœ“ Project initialized successfully!\n\n") - fmt.Printf("Files created:\n") - fmt.Printf(" - mcpgen.yaml (code generation configuration)\n") - fmt.Printf(" - schema.yaml (MCP API specification)\n\n") - fmt.Printf("Next steps:\n") - fmt.Printf(" cd %s\n", name) - fmt.Printf(" # Edit schema.yaml to define your tools, resources, and prompts\n") - fmt.Printf(" mcpgen generate\n") - - return nil -} diff --git a/third_party/mcpgen/mcp/omittable.go b/third_party/mcpgen/mcp/omittable.go deleted file mode 100644 index 1097cccc5..000000000 --- a/third_party/mcpgen/mcp/omittable.go +++ /dev/null @@ -1,117 +0,0 @@ -package mcp - -import ( - "encoding/json" - "fmt" -) - -// Omittable represents a value that can be in one of three states: -// 1. Not set (field was not provided in JSON) -// 2. Explicitly set to null -// 3. Set to a value -// -// This is useful for distinguishing between "don't update this field" (not set) -// and "set this field to null" (explicitly null) in update operations. -// -// Example usage: -// -// type UpdateUserInput struct { -// Name Omittable[string] `json:"name,omitempty"` -// Email Omittable[string] `json:"email,omitempty"` -// } -// -// func (r *Resolver) UpdateUser(input UpdateUserInput) { -// if input.Name.IsSet() { -// if input.Name.IsNull() { -// // Set name to null -// } else { -// // Update name to input.Name.Value() -// } -// } -// // If !IsSet(), don't touch the name field -// } -type Omittable[T any] struct { - value *T - isSet bool -} - -func NewOmittable[T any](value T) Omittable[T] { - return Omittable[T]{ - value: &value, - isSet: true, - } -} - -func NewOmittableNull[T any]() Omittable[T] { - return Omittable[T]{ - value: nil, - isSet: true, - } -} - -// IsSet returns true if the field was provided in the input (either null or a value). -func (o Omittable[T]) IsSet() bool { - return o.isSet -} - -// IsNull returns true if the field was explicitly set to null. -// Returns false if the field was not set or has a value. -func (o Omittable[T]) IsNull() bool { - return o.isSet && o.value == nil -} - -// Value returns the value and a boolean indicating if it has a non-null value. -// If the field is not set or is null, returns the zero value and false. -func (o Omittable[T]) Value() (T, bool) { - if o.value != nil { - return *o.value, true - } - var zero T - return zero, false -} - -func (o Omittable[T]) ValueOrZero() T { - if o.value != nil { - return *o.value - } - var zero T - return zero -} - -func (o Omittable[T]) Ptr() *T { - return o.value -} - -// UnmarshalJSON implements json.Unmarshaler. -func (o *Omittable[T]) UnmarshalJSON(data []byte) error { - o.isSet = true - - // Handle explicit null - if string(data) == "null" { - o.value = nil - return nil - } - - // Unmarshal the actual value - var value T - if err := json.Unmarshal(data, &value); err != nil { - return fmt.Errorf("failed to unmarshal omittable value: %w", err) - } - - o.value = &value - return nil -} - -// MarshalJSON implements json.Marshaler. -func (o Omittable[T]) MarshalJSON() ([]byte, error) { - // Note: When marshaling structs with Omittable fields, use *Omittable[T] - // if you need omitempty to work correctly. With value types, omitempty - // doesn't work well with custom MarshalJSON. - // For MCP use cases (unmarshaling input), this is not typically an issue. - - if !o.isSet || o.value == nil { - return []byte("null"), nil - } - - return json.Marshal(*o.value) -} diff --git a/third_party/mcpgen/mcp/omittable_test.go b/third_party/mcpgen/mcp/omittable_test.go deleted file mode 100644 index d17c13be9..000000000 --- a/third_party/mcpgen/mcp/omittable_test.go +++ /dev/null @@ -1,201 +0,0 @@ -package mcp - -import ( - "encoding/json" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestOmittable_NotSet(t *testing.T) { - var o Omittable[string] - - assert.False(t, o.IsSet()) - assert.False(t, o.IsNull()) - - value, ok := o.Value() - assert.False(t, ok) - assert.Equal(t, "", value) -} - -func TestOmittable_SetToValue(t *testing.T) { - o := NewOmittable("hello") - - assert.True(t, o.IsSet()) - assert.False(t, o.IsNull()) - - value, ok := o.Value() - assert.True(t, ok) - assert.Equal(t, "hello", value) - assert.Equal(t, "hello", o.ValueOrZero()) - - ptr := o.Ptr() - require.NotNil(t, ptr) - assert.Equal(t, "hello", *ptr) -} - -func TestOmittable_SetToNull(t *testing.T) { - o := NewOmittableNull[string]() - - assert.True(t, o.IsSet()) - assert.True(t, o.IsNull()) - - value, ok := o.Value() - assert.False(t, ok) - assert.Equal(t, "", value) - assert.Equal(t, "", o.ValueOrZero()) - assert.Nil(t, o.Ptr()) -} - -func TestOmittable_UnmarshalJSON_NotProvided(t *testing.T) { - type Input struct { - Name Omittable[string] `json:"name,omitempty"` - Email Omittable[string] `json:"email,omitempty"` - } - - jsonData := `{"name": "John"}` - var input Input - require.NoError(t, json.Unmarshal([]byte(jsonData), &input)) - - assert.True(t, input.Name.IsSet()) - name, ok := input.Name.Value() - assert.True(t, ok) - assert.Equal(t, "John", name) - - assert.False(t, input.Email.IsSet()) -} - -func TestOmittable_UnmarshalJSON_ExplicitNull(t *testing.T) { - type Input struct { - Name Omittable[string] `json:"name,omitempty"` - Email Omittable[string] `json:"email,omitempty"` - } - - jsonData := `{"name": null, "email": "test@example.com"}` - var input Input - require.NoError(t, json.Unmarshal([]byte(jsonData), &input)) - - assert.True(t, input.Name.IsSet()) - assert.True(t, input.Name.IsNull()) - - assert.True(t, input.Email.IsSet()) - email, ok := input.Email.Value() - assert.True(t, ok) - assert.Equal(t, "test@example.com", email) -} - -func TestOmittable_UnmarshalJSON_WithValue(t *testing.T) { - type Input struct { - Count Omittable[int] `json:"count,omitempty"` - } - - jsonData := `{"count": 42}` - var input Input - require.NoError(t, json.Unmarshal([]byte(jsonData), &input)) - - assert.True(t, input.Count.IsSet()) - assert.False(t, input.Count.IsNull()) - count, ok := input.Count.Value() - assert.True(t, ok) - assert.Equal(t, 42, count) -} - -func TestOmittable_MarshalJSON_NotSet(t *testing.T) { - type Output struct { - Name Omittable[string] `json:"name,omitempty"` - } - - output := Output{} - data, err := json.Marshal(output) - require.NoError(t, err) - - assert.JSONEq(t, `{"name":null}`, string(data)) -} - -func TestOmittable_MarshalJSON_Null(t *testing.T) { - type Output struct { - Name Omittable[string] `json:"name,omitempty"` - } - - output := Output{ - Name: NewOmittableNull[string](), - } - data, err := json.Marshal(output) - require.NoError(t, err) - - assert.JSONEq(t, `{"name":null}`, string(data)) -} - -func TestOmittable_MarshalJSON_WithValue(t *testing.T) { - type Output struct { - Name Omittable[string] `json:"name,omitempty"` - } - - output := Output{ - Name: NewOmittable("Alice"), - } - data, err := json.Marshal(output) - require.NoError(t, err) - - assert.JSONEq(t, `{"name":"Alice"}`, string(data)) -} - -func TestOmittable_ComplexTypes(t *testing.T) { - type Person struct { - Name string `json:"name"` - Age int `json:"age"` - } - - type Input struct { - Person Omittable[Person] `json:"person,omitempty"` - } - - t.Run("with value", func(t *testing.T) { - jsonData := `{"person": {"name": "John", "age": 30}}` - var input Input - require.NoError(t, json.Unmarshal([]byte(jsonData), &input)) - - assert.True(t, input.Person.IsSet()) - person, ok := input.Person.Value() - assert.True(t, ok) - assert.Equal(t, "John", person.Name) - assert.Equal(t, 30, person.Age) - }) - - t.Run("with null", func(t *testing.T) { - jsonData := `{"person": null}` - var input Input - require.NoError(t, json.Unmarshal([]byte(jsonData), &input)) - - assert.True(t, input.Person.IsSet()) - assert.True(t, input.Person.IsNull()) - }) -} - -func TestOmittable_Pointers(t *testing.T) { - type Input struct { - Name Omittable[*string] `json:"name,omitempty"` - } - - t.Run("with value", func(t *testing.T) { - jsonData := `{"name": "hello"}` - var input Input - require.NoError(t, json.Unmarshal([]byte(jsonData), &input)) - - assert.True(t, input.Name.IsSet()) - value, ok := input.Name.Value() - assert.True(t, ok) - require.NotNil(t, value) - assert.Equal(t, "hello", *value) - }) - - t.Run("with null", func(t *testing.T) { - jsonData := `{"name": null}` - var input Input - require.NoError(t, json.Unmarshal([]byte(jsonData), &input)) - - assert.True(t, input.Name.IsSet()) - assert.True(t, input.Name.IsNull()) - }) -} diff --git a/third_party/mcpgen/mcp/recover.go b/third_party/mcpgen/mcp/recover.go deleted file mode 100644 index 5ea5be0dc..000000000 --- a/third_party/mcpgen/mcp/recover.go +++ /dev/null @@ -1,63 +0,0 @@ -package mcp - -import ( - "context" - "errors" - "fmt" - "os" - "runtime/debug" -) - -// RecoverFunc is called when a tool handler panics. It receives the recovered -// value (whatever was passed to panic) and returns an error to be reported to -// the client. -// -// This matches the signature and semantics of gqlgen's RecoverFunc. -// -// Example: -// -// server.New(resolver, server.WithRecoverFunc(func(ctx context.Context, err any) error { -// log.Error("tool panic", "err", err) -// return errors.New("internal server error") -// })) -type RecoverFunc func(ctx context.Context, err any) error - -// DefaultRecoverFunc prints the panic and stack trace to stderr and returns a -// generic internal error. This matches gqlgen's DefaultRecover behavior. -func DefaultRecoverFunc(_ context.Context, err any) error { - fmt.Fprintln(os.Stderr, err) - fmt.Fprintln(os.Stderr) - debug.PrintStack() - return errors.New("internal system error") -} - -// Option configures the generated MCP server. -type Option func(*Options) - -// Options holds configuration for the generated MCP server. -type Options struct { - RecoverFunc RecoverFunc -} - -// WithRecoverFunc sets the panic recover function for tool handlers. -// The recover function is called when a tool handler panics, and its return -// value is sent to the client in place of the panic. -func WithRecoverFunc(fn RecoverFunc) Option { - return func(o *Options) { - o.RecoverFunc = fn - } -} - -// ApplyOptions applies the given options to an Options struct. -// If RecoverFunc is nil after applying options, it is set to DefaultRecoverFunc: -// recovery is always enabled, matching gqlgen's behavior. -func ApplyOptions(opts []Option) Options { - var o Options - for _, opt := range opts { - opt(&o) - } - if o.RecoverFunc == nil { - o.RecoverFunc = DefaultRecoverFunc - } - return o -} diff --git a/third_party/mcpgen/mcp/recover_test.go b/third_party/mcpgen/mcp/recover_test.go deleted file mode 100644 index f029af308..000000000 --- a/third_party/mcpgen/mcp/recover_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package mcp - -import ( - "context" - "errors" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestApplyOptions(t *testing.T) { - t.Run("no options uses default recover func", func(t *testing.T) { - opts := ApplyOptions(nil) - assert.NotNil(t, opts.RecoverFunc) - }) - - t.Run("nil recover func falls back to default", func(t *testing.T) { - opts := ApplyOptions([]Option{WithRecoverFunc(nil)}) - assert.NotNil(t, opts.RecoverFunc) - }) - - t.Run("with custom recover func", func(t *testing.T) { - fn := func(_ context.Context, _ any) error { - return errors.New("sanitized") - } - opts := ApplyOptions([]Option{WithRecoverFunc(fn)}) - assert.NotNil(t, opts.RecoverFunc) - - err := opts.RecoverFunc(context.Background(), "boom") - assert.Equal(t, "sanitized", err.Error()) - }) - - t.Run("recover func receives raw panic value", func(t *testing.T) { - var captured any - fn := func(_ context.Context, err any) error { - captured = err - return nil - } - opts := ApplyOptions([]Option{WithRecoverFunc(fn)}) - - opts.RecoverFunc(context.Background(), 42) - assert.Equal(t, 42, captured) - - opts.RecoverFunc(context.Background(), "string panic") - assert.Equal(t, "string panic", captured) - - original := errors.New("error panic") - opts.RecoverFunc(context.Background(), original) - assert.Equal(t, original, captured) - }) -} diff --git a/third_party/mcpgen/mcp/schema.go b/third_party/mcpgen/mcp/schema.go deleted file mode 100644 index 4b1358877..000000000 --- a/third_party/mcpgen/mcp/schema.go +++ /dev/null @@ -1,77 +0,0 @@ -package mcp - -import ( - "context" - "encoding/json" - - "github.com/google/jsonschema-go/jsonschema" - "github.com/modelcontextprotocol/go-sdk/mcp" -) - -// MustUnmarshalSchema unmarshals a JSON schema string into a jsonschema.Schema -// Panics if unmarshaling fails, providing compile-time safety for schema definitions -func MustUnmarshalSchema(schemaJSON string) *jsonschema.Schema { - var schema jsonschema.Schema - if err := json.Unmarshal([]byte(schemaJSON), &schema); err != nil { - panic("invalid schema JSON: " + err.Error()) - } - return &schema -} - -// PromptHandlerFor is a typed prompt handler that accepts structured arguments. -// Similar to mcp.ToolHandlerFor, this allows prompts to work with typed Go structs -// instead of raw map[string]string. -// -// The Args type parameter must be a struct or map type. Arguments will be automatically -// unmarshaled from the prompt request's Arguments map into the Args type. -// -// Example: -// -// type TaskArgs struct { -// Topic string `json:"topic"` -// Detailed bool `json:"detailed"` -// } -// -// func (r *Resolver) TaskHelpPrompt(ctx context.Context, req *mcp.GetPromptRequest, args TaskArgs) (*mcp.GetPromptResult, error) { -// // args.Topic and args.Detailed are already parsed -// return &mcp.GetPromptResult{...}, nil -// } -type PromptHandlerFor[Args any] func(context.Context, *mcp.GetPromptRequest, Args) (*mcp.GetPromptResult, error) - -// AddPrompt is a generic wrapper around Server.AddPrompt that provides type-safe argument handling. -// It automatically converts the prompt arguments from map[string]string into the typed Args parameter. -// -// This matches the ergonomics of mcp.AddTool for a consistent API experience across tools and prompts. -// -// The Args type must be a struct with string fields or map[string]string. If it's a struct, the fields -// will be populated from the arguments map based on their json tags. -// -// Example: -// -// type HelpArgs struct { -// Topic string `json:"topic"` -// } -// -// mcp.AddPrompt(server, &mcp.Prompt{ -// Name: "help", -// Description: "Get help", -// }, resolver.HelpPrompt) // HelpPrompt receives typed HelpArgs -func AddPrompt[Args any](s *mcp.Server, p *mcp.Prompt, h PromptHandlerFor[Args]) { - s.AddPrompt(p, func(ctx context.Context, req *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { - var args Args - - // Convert map[string]string to typed Args using JSON as the intermediary. - // This properly handles json tags and field mapping. - if len(req.Params.Arguments) > 0 { - argsBytes, err := json.Marshal(req.Params.Arguments) - if err != nil { - return nil, err - } - if err := json.Unmarshal(argsBytes, &args); err != nil { - return nil, err - } - } - - return h(ctx, req, args) - }) -}