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) - }) -}