Annotate MCP tools with titles and hints
Claude and other MCP clients use title, readOnlyHint, and destructiveHint to present reads, writes, and deletes accurately. Add a title to every tool, mark missing delete/unlink/cancel/void tools as destructive, and teach mcpgen to emit those annotations (including destructiveHint: false for non-destructive writes). Temporary third_party/mcpgen fork until title support lands upstream. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr> Signed-off-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
@@ -25,17 +25,33 @@ go generate ./pkg/server/api/mcp/v1
|
|||||||
```yaml
|
```yaml
|
||||||
tools:
|
tools:
|
||||||
- name: listThirdParties
|
- name: listThirdParties
|
||||||
|
title: List Third Parties
|
||||||
description: List all thirdParties for the organization
|
description: List all thirdParties for the organization
|
||||||
hints:
|
hints:
|
||||||
readonly: true
|
readonly: true
|
||||||
idempotent: true
|
idempotent: true
|
||||||
destructive: false
|
|
||||||
inputSchema:
|
inputSchema:
|
||||||
$ref: "#/components/schemas/ListThirdPartiesInput"
|
$ref: "#/components/schemas/ListThirdPartiesInput"
|
||||||
outputSchema:
|
outputSchema:
|
||||||
$ref: "#/components/schemas/ListThirdPartiesOutput"
|
$ref: "#/components/schemas/ListThirdPartiesOutput"
|
||||||
|
- name: deleteThirdParty
|
||||||
|
title: Delete Third Party
|
||||||
|
description: Delete a thirdParty
|
||||||
|
hints:
|
||||||
|
readonly: false
|
||||||
|
destructive: true
|
||||||
|
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`.
|
||||||
|
|
||||||
Input/output schemas reference `components/schemas`. Map custom Go types with the `go.probo.inc/mcpgen/type` extension:
|
Input/output schemas reference `components/schemas`. Map custom Go types with the `go.probo.inc/mcpgen/type` extension:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -272,3 +272,5 @@ tool (
|
|||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/elimity-com/scim => github.com/getprobo/scim v0.0.0-20260309220528-a952b258e8d3
|
replace github.com/elimity-com/scim => github.com/getprobo/scim v0.0.0-20260309220528-a952b258e8d3
|
||||||
|
|
||||||
|
replace go.probo.inc/mcpgen => ./third_party/mcpgen
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -651,8 +651,6 @@ 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/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 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
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 h1:6azC+pD678Vjju8yXnMDHCZJ+HzFaEmL3sCryiezTIA=
|
||||||
go.step.sm/crypto v0.77.7/go.mod h1:OW/2sEHwTtDKq70PvSQ5B0JGy/CrLyDKOiVy3YvZMTQ=
|
go.step.sm/crypto v0.77.7/go.mod h1:OW/2sEHwTtDKq70PvSQ5B0JGy/CrLyDKOiVy3YvZMTQ=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
20
third_party/mcpgen/LICENSE
vendored
Normal file
20
third_party/mcpgen/LICENSE
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
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.
|
||||||
381
third_party/mcpgen/README.md
vendored
Normal file
381
third_party/mcpgen/README.md
vendored
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
# 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)
|
||||||
12
third_party/mcpgen/THIRD_PARTY.md
vendored
Normal file
12
third_party/mcpgen/THIRD_PARTY.md
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# 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.
|
||||||
21
third_party/mcpgen/go.mod
vendored
Normal file
21
third_party/mcpgen/go.mod
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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
|
||||||
|
)
|
||||||
32
third_party/mcpgen/go.sum
vendored
Normal file
32
third_party/mcpgen/go.sum
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
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=
|
||||||
1292
third_party/mcpgen/internal/codegen/generator.go
vendored
Normal file
1292
third_party/mcpgen/internal/codegen/generator.go
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1387
third_party/mcpgen/internal/codegen/generator_test.go
vendored
Normal file
1387
third_party/mcpgen/internal/codegen/generator_test.go
vendored
Normal file
File diff suppressed because it is too large
Load Diff
282
third_party/mcpgen/internal/codegen/integration_test.go
vendored
Normal file
282
third_party/mcpgen/internal/codegen/integration_test.go
vendored
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
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")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
191
third_party/mcpgen/internal/codegen/parser.go
vendored
Normal file
191
third_party/mcpgen/internal/codegen/parser.go
vendored
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
484
third_party/mcpgen/internal/codegen/parser_test.go
vendored
Normal file
484
third_party/mcpgen/internal/codegen/parser_test.go
vendored
Normal file
@@ -0,0 +1,484 @@
|
|||||||
|
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")
|
||||||
|
}
|
||||||
58
third_party/mcpgen/internal/codegen/templates/resolver.gotpl
vendored
Normal file
58
third_party/mcpgen/internal/codegen/templates/resolver.gotpl
vendored
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
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}}
|
||||||
22
third_party/mcpgen/internal/codegen/templates/resolver_struct.gotpl
vendored
Normal file
22
third_party/mcpgen/internal/codegen/templates/resolver_struct.gotpl
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
175
third_party/mcpgen/internal/codegen/templates/server.gotpl
vendored
Normal file
175
third_party/mcpgen/internal/codegen/templates/server.gotpl
vendored
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
// 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}}
|
||||||
222
third_party/mcpgen/internal/codegen/testdata/all_primitives.yaml
vendored
Normal file
222
third_party/mcpgen/internal/codegen/testdata/all_primitives.yaml
vendored
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
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
|
||||||
31
third_party/mcpgen/internal/codegen/testdata/config_based_types.golden
vendored
Normal file
31
third_party/mcpgen/internal/codegen/testdata/config_based_types.golden
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// 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"`
|
||||||
|
}
|
||||||
45
third_party/mcpgen/internal/codegen/testdata/config_based_types.yaml
vendored
Normal file
45
third_party/mcpgen/internal/codegen/testdata/config_based_types.yaml
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
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"
|
||||||
185
third_party/mcpgen/internal/codegen/testdata/custom_types.yaml
vendored
Normal file
185
third_party/mcpgen/internal/codegen/testdata/custom_types.yaml
vendored
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
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
|
||||||
652
third_party/mcpgen/internal/codegen/types.go
vendored
Normal file
652
third_party/mcpgen/internal/codegen/types.go
vendored
Normal file
@@ -0,0 +1,652 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
1048
third_party/mcpgen/internal/codegen/types_test.go
vendored
Normal file
1048
third_party/mcpgen/internal/codegen/types_test.go
vendored
Normal file
File diff suppressed because it is too large
Load Diff
208
third_party/mcpgen/internal/config/config.go
vendored
Normal file
208
third_party/mcpgen/internal/config/config.go
vendored
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
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 != ""
|
||||||
|
}
|
||||||
114
third_party/mcpgen/internal/config/spec.go
vendored
Normal file
114
third_party/mcpgen/internal/config/spec.go
vendored
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
85
third_party/mcpgen/internal/schema/schema.go
vendored
Normal file
85
third_party/mcpgen/internal/schema/schema.go
vendored
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
185
third_party/mcpgen/main.go
vendored
Normal file
185
third_party/mcpgen/main.go
vendored
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
117
third_party/mcpgen/mcp/omittable.go
vendored
Normal file
117
third_party/mcpgen/mcp/omittable.go
vendored
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
201
third_party/mcpgen/mcp/omittable_test.go
vendored
Normal file
201
third_party/mcpgen/mcp/omittable_test.go
vendored
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
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())
|
||||||
|
})
|
||||||
|
}
|
||||||
63
third_party/mcpgen/mcp/recover.go
vendored
Normal file
63
third_party/mcpgen/mcp/recover.go
vendored
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
51
third_party/mcpgen/mcp/recover_test.go
vendored
Normal file
51
third_party/mcpgen/mcp/recover_test.go
vendored
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
77
third_party/mcpgen/mcp/schema.go
vendored
Normal file
77
third_party/mcpgen/mcp/schema.go
vendored
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user