Set full MCP tool annotation hints in schema
Fill in readonly, destructive, idempotent, and openWorld on every tool so clients can rely on explicit values instead of MCP defaults (destructive and openWorld both default to true when omitted). Drop the temporary mcpgen vendor from this change; that lands in a separate pull request. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
This commit is contained in:
@@ -30,27 +30,49 @@ tools:
|
||||
hints:
|
||||
readonly: true
|
||||
idempotent: true
|
||||
openWorld: false
|
||||
inputSchema:
|
||||
$ref: "#/components/schemas/ListThirdPartiesInput"
|
||||
outputSchema:
|
||||
$ref: "#/components/schemas/ListThirdPartiesOutput"
|
||||
- name: addThirdParty
|
||||
title: Add Third Party
|
||||
description: Add a new thirdParty to the organization
|
||||
hints:
|
||||
readonly: false
|
||||
destructive: false
|
||||
idempotent: false
|
||||
openWorld: false
|
||||
inputSchema:
|
||||
$ref: "#/components/schemas/AddThirdPartyInput"
|
||||
outputSchema:
|
||||
$ref: "#/components/schemas/AddThirdPartyOutput"
|
||||
- name: deleteThirdParty
|
||||
title: Delete Third Party
|
||||
description: Delete a thirdParty
|
||||
hints:
|
||||
readonly: false
|
||||
destructive: true
|
||||
idempotent: true
|
||||
openWorld: false
|
||||
inputSchema:
|
||||
$ref: "#/components/schemas/DeleteThirdPartyInput"
|
||||
outputSchema:
|
||||
$ref: "#/components/schemas/DeleteThirdPartyOutput"
|
||||
```
|
||||
|
||||
`title` is the human-readable display name (emitted as MCP `title` /
|
||||
`annotations.title`). `hints.readonly` and `hints.destructive` map to
|
||||
`readOnlyHint` and `destructiveHint` so clients can distinguish reads, writes,
|
||||
and deletes. Every delete/remove/unlink/cancel/void tool must set
|
||||
`destructive: true`.
|
||||
`title` is the human-readable display name (MCP `title` / `annotations.title`).
|
||||
|
||||
`hints` map to MCP tool annotations. Set all of them explicitly — several
|
||||
defaults are surprising (`destructiveHint` defaults to **true**,
|
||||
`openWorldHint` defaults to **true**):
|
||||
|
||||
| Hint | Reads | Additive writes | Deletes / regenerates |
|
||||
|---|---|---|---|
|
||||
| `readonly` | `true` | `false` | `false` |
|
||||
| `destructive` | omit | `false` | `true` |
|
||||
| `idempotent` | `true` | `false` for creates, `true` for updates | `true` |
|
||||
| `openWorld` | `false` | `false` (unless the tool calls out, e.g. `vetThirdParty`) | `false` |
|
||||
|
||||
Input/output schemas reference `components/schemas`. Map custom Go types with the `go.probo.inc/mcpgen/type` extension:
|
||||
|
||||
|
||||
2
go.mod
2
go.mod
@@ -272,5 +272,3 @@ tool (
|
||||
)
|
||||
|
||||
replace github.com/elimity-com/scim => github.com/getprobo/scim v0.0.0-20260309220528-a952b258e8d3
|
||||
|
||||
replace go.probo.inc/mcpgen => ./third_party/mcpgen
|
||||
|
||||
2
go.sum
2
go.sum
@@ -651,6 +651,8 @@ go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/
|
||||
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||
go.probo.inc/mcpgen v0.0.0-20260428172408-1496ba9b4619 h1:LHOdoF7kYRXFtSP97eWpF1dIf0dBLrunRLOeU/pXt9c=
|
||||
go.probo.inc/mcpgen v0.0.0-20260428172408-1496ba9b4619/go.mod h1:HunWQGqLdMocExJh4tWaX7p+uRZ9GlKvBvOXHaFW6vM=
|
||||
go.step.sm/crypto v0.77.7 h1:6azC+pD678Vjju8yXnMDHCZJ+HzFaEmL3sCryiezTIA=
|
||||
go.step.sm/crypto v0.77.7/go.mod h1:OW/2sEHwTtDKq70PvSQ5B0JGy/CrLyDKOiVy3YvZMTQ=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
20
third_party/mcpgen/LICENSE
vendored
20
third_party/mcpgen/LICENSE
vendored
@@ -1,20 +0,0 @@
|
||||
Copyright 2025 Probo Inc
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
“Software”), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
381
third_party/mcpgen/README.md
vendored
381
third_party/mcpgen/README.md
vendored
@@ -1,381 +0,0 @@
|
||||
# mcpgen
|
||||
|
||||
## Overview
|
||||
|
||||
mcpgen is a code generator for Model Context Protocol (MCP) servers in Go, inspired by [gqlgen](https://github.com/99designs/gqlgen).
|
||||
|
||||
mcpgen takes a schema-first approach to building MCP servers. Define your tools, resources, and prompts in a YAML configuration file with JSON Schema definitions, and mcpgen generates type-safe Go code including:
|
||||
|
||||
- Type-safe Go structs from JSON Schemas
|
||||
- MCP server boilerplate with the official [go-sdk](https://github.com/modelcontextprotocol/go-sdk)
|
||||
- Handler function stubs ready for your business logic
|
||||
|
||||
## Features
|
||||
|
||||
- **Schema-First Development**: Define MCP primitives (tools, resources, prompts) in YAML with JSON Schema
|
||||
- **Type-Safe Code Generation**: Generate Go structs from JSON Schema Draft 2020-12
|
||||
- **Custom Type Mapping**: Use your own Go types instead of generated ones (like gqlgen)
|
||||
- **Omittable Fields**: Distinguish between "not set", "null", and "value" with `go.probo.inc/mcpgen/omittable` (like gqlgen's `@goField(omittable: true)`)
|
||||
- **Official SDK Integration**: Uses the official `modelcontextprotocol/go-sdk`
|
||||
- **Handler Preservation**: Regeneration preserves your handler implementations
|
||||
- **gqlgen-Inspired**: Familiar workflow if you've used gqlgen
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
go install go.probo.inc/mcpgen@latest
|
||||
```
|
||||
|
||||
Or build from source:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/probo-inc/mcpgen
|
||||
cd mcpgen
|
||||
go build -o mcpgen
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Initialize a new project
|
||||
|
||||
```bash
|
||||
mcpgen init my-mcp-server
|
||||
cd my-mcp-server
|
||||
```
|
||||
|
||||
This creates:
|
||||
```
|
||||
my-mcp-server/
|
||||
├── mcpgen.yaml # Configuration file
|
||||
├── schemas/ # JSON Schema definitions
|
||||
│ └── example_input.json
|
||||
├── main.go # Entry point
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### 2. Define your MCP primitives
|
||||
|
||||
Edit `mcpgen.yaml`:
|
||||
|
||||
```yaml
|
||||
server:
|
||||
name: my-mcp-server
|
||||
version: 1.0.0
|
||||
|
||||
tools:
|
||||
- name: calculate
|
||||
description: Perform arithmetic operations
|
||||
input_schema: schemas/calculate_input.json
|
||||
|
||||
resources:
|
||||
- uri: docs://readme
|
||||
name: Project README
|
||||
description: The project README file
|
||||
mime_type: text/markdown
|
||||
|
||||
prompts:
|
||||
- name: greeting
|
||||
description: A friendly greeting
|
||||
arguments:
|
||||
- name: name
|
||||
description: Name of person to greet
|
||||
required: false
|
||||
```
|
||||
|
||||
### 3. Create JSON Schemas
|
||||
|
||||
Define schemas in the `schemas/` directory:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"operation": {
|
||||
"type": "string",
|
||||
"enum": ["add", "subtract", "multiply", "divide"]
|
||||
},
|
||||
"a": {
|
||||
"type": "number",
|
||||
"description": "First operand"
|
||||
},
|
||||
"b": {
|
||||
"type": "number",
|
||||
"description": "Second operand"
|
||||
}
|
||||
},
|
||||
"required": ["operation", "a", "b"]
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Generate code
|
||||
|
||||
```bash
|
||||
mcpgen generate
|
||||
```
|
||||
|
||||
This generates:
|
||||
- `generated/models.go` - Type-safe Go structs
|
||||
- `generated/server.go` - MCP server setup
|
||||
- `generated/resolver.go` - Handler stubs (first time only)
|
||||
|
||||
### 5. Implement handlers
|
||||
|
||||
Edit `generated/resolver.go`:
|
||||
|
||||
```go
|
||||
func (r *Resolver) Calculate(ctx context.Context, req *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, map[string]any, error) {
|
||||
operation := args["operation"].(string)
|
||||
a := args["a"].(float64)
|
||||
b := args["b"].(float64)
|
||||
|
||||
var result float64
|
||||
switch operation {
|
||||
case "add":
|
||||
result = a + b
|
||||
case "subtract":
|
||||
result = a - b
|
||||
case "multiply":
|
||||
result = a * b
|
||||
case "divide":
|
||||
if b == 0 {
|
||||
return nil, nil, fmt.Errorf("division by zero")
|
||||
}
|
||||
result = a / b
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{
|
||||
Text: fmt.Sprintf("Result: %f", result),
|
||||
},
|
||||
},
|
||||
}, map[string]any{"result": result}, nil
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Build and run
|
||||
|
||||
```bash
|
||||
go mod init my-mcp-server
|
||||
go mod tidy
|
||||
go build -o server
|
||||
./server
|
||||
```
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
### Server Configuration
|
||||
|
||||
```yaml
|
||||
server:
|
||||
name: my-server # Required: Server name
|
||||
version: 1.0.0 # Required: Server version
|
||||
```
|
||||
|
||||
### Code Generation Options
|
||||
|
||||
```yaml
|
||||
exec:
|
||||
filename: generated/server.go # Server code output
|
||||
package: generated # Package name
|
||||
|
||||
model:
|
||||
filename: generated/models.go # Models output
|
||||
package: generated # Package name
|
||||
|
||||
resolver:
|
||||
filename: generated/resolver.go # Resolver stubs output
|
||||
type: Resolver # Resolver type name
|
||||
package: generated # Package name
|
||||
preserve_resolver: true # Don't overwrite on regeneration
|
||||
```
|
||||
|
||||
### Tools
|
||||
|
||||
```yaml
|
||||
tools:
|
||||
- name: tool_name # Required: Tool identifier
|
||||
description: Tool description # Optional: Human-readable description
|
||||
input_schema: schemas/input.json # Required: JSON Schema for input
|
||||
output_schema: schemas/output.json # Optional: JSON Schema for output
|
||||
```
|
||||
|
||||
### Resources
|
||||
|
||||
Static resources:
|
||||
|
||||
```yaml
|
||||
resources:
|
||||
- uri: docs://readme # Required: Resource URI
|
||||
name: README # Required: Display name
|
||||
description: Project README # Optional
|
||||
mime_type: text/markdown # Optional
|
||||
```
|
||||
|
||||
Resource templates (dynamic URIs):
|
||||
|
||||
```yaml
|
||||
resources:
|
||||
- uri_template: users://{id}/profile # Required: URI template
|
||||
name: User Profile # Required
|
||||
description: User profile data # Optional
|
||||
mime_type: application/json # Optional
|
||||
uri_params: # Parameters from template
|
||||
- name: id
|
||||
type: string
|
||||
description: User ID
|
||||
```
|
||||
|
||||
### Prompts
|
||||
|
||||
```yaml
|
||||
prompts:
|
||||
- name: prompt_name # Required: Prompt identifier
|
||||
description: Description # Optional
|
||||
arguments: # Optional: Prompt arguments
|
||||
- name: arg_name
|
||||
description: Arg description
|
||||
required: true
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### `mcpgen init [name]`
|
||||
|
||||
Initialize a new MCP server project with example configuration.
|
||||
|
||||
```bash
|
||||
mcpgen init my-server
|
||||
```
|
||||
|
||||
### `mcpgen generate`
|
||||
|
||||
Generate code from `mcpgen.yaml` configuration.
|
||||
|
||||
```bash
|
||||
mcpgen generate
|
||||
|
||||
# Specify custom config file
|
||||
mcpgen generate --config custom-config.yaml
|
||||
```
|
||||
|
||||
### `mcpgen version`
|
||||
|
||||
Print mcpgen version.
|
||||
|
||||
```bash
|
||||
mcpgen version
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Configuration Loading**: mcpgen reads your `mcpgen.yaml` file
|
||||
2. **Schema Loading**: JSON Schemas are loaded and `$ref` references resolved
|
||||
3. **Type Generation**: Go structs are generated from JSON Schemas
|
||||
4. **Server Generation**: MCP server boilerplate is generated with tool/resource/prompt registration
|
||||
5. **Resolver Generation**: Handler stubs are generated (only if they don't exist)
|
||||
|
||||
## MCP Primitives
|
||||
|
||||
### Tools
|
||||
|
||||
Tools let LLMs interact with external systems. Each tool has:
|
||||
- **Name**: Unique identifier (alphanumeric, underscore, dash, dot)
|
||||
- **Description**: What the tool does
|
||||
- **Input Schema**: JSON Schema defining parameters (required)
|
||||
- **Output Schema**: JSON Schema for result validation (optional)
|
||||
|
||||
### Resources
|
||||
|
||||
Resources provide context to LLMs via URIs:
|
||||
- **Static Resources**: Fixed URI (e.g., `docs://readme`)
|
||||
- **Resource Templates**: Dynamic URIs (e.g., `users://{id}/profile`)
|
||||
|
||||
### Prompts
|
||||
|
||||
Prompts are reusable templates for LLM interactions with optional arguments.
|
||||
|
||||
## Comparison with gqlgen
|
||||
|
||||
| Feature | gqlgen | mcpgen |
|
||||
|---------|--------|--------|
|
||||
| **Schema Language** | GraphQL SDL | JSON Schema |
|
||||
| **Protocol** | GraphQL | MCP (JSON-RPC 2.0) |
|
||||
| **Core Primitives** | Queries, Mutations, Subscriptions | Tools, Resources, Prompts |
|
||||
| **Generation** | Resolvers, models | Handlers, models |
|
||||
| **Schema-first** | ✅ | ✅ |
|
||||
| **Preserve implementations** | ✅ | ✅ |
|
||||
| **Type safety** | ✅ | ✅ |
|
||||
|
||||
## Custom Type Mapping
|
||||
|
||||
You can use your own Go types instead of generated ones, similar to gqlgen's model binding.
|
||||
|
||||
### Using Schema Annotations (Recommended)
|
||||
|
||||
Add `go.probo.inc/mcpgen/type` annotations in your JSON Schema:
|
||||
|
||||
```yaml
|
||||
components:
|
||||
schemas:
|
||||
# Use time.Time for timestamps
|
||||
Timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
go.probo.inc/mcpgen/type: time.Time
|
||||
|
||||
# Use UUID package
|
||||
UUID:
|
||||
type: string
|
||||
format: uuid
|
||||
go.probo.inc/mcpgen/type: github.com/google/uuid.UUID
|
||||
|
||||
# Use your own domain models
|
||||
User:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
go.probo.inc/mcpgen/type: github.com/myorg/models.User
|
||||
```
|
||||
|
||||
When you reference these schemas, mcpgen will:
|
||||
- Skip generating types for them
|
||||
- Use your custom types instead
|
||||
- Automatically add necessary imports
|
||||
|
||||
See [docs/custom-types.md](docs/custom-types.md) for full documentation.
|
||||
|
||||
## Examples
|
||||
|
||||
See the `examples/` directory for complete working examples.
|
||||
|
||||
## Development
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
go build -o mcpgen
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions welcome. Please submit a Pull Request.
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see LICENSE file for details.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- Inspired by [gqlgen](https://github.com/99designs/gqlgen)
|
||||
- Uses the official [Model Context Protocol Go SDK](https://github.com/modelcontextprotocol/go-sdk)
|
||||
12
third_party/mcpgen/THIRD_PARTY.md
vendored
12
third_party/mcpgen/THIRD_PARTY.md
vendored
@@ -1,12 +0,0 @@
|
||||
# Vendored mcpgen
|
||||
|
||||
This is a temporary fork of [getprobo/mcpgen](https://github.com/getprobo/mcpgen)
|
||||
(module `go.probo.inc/mcpgen`) with tool annotation improvements:
|
||||
|
||||
- `title` field on tools (emitted as `Tool.Title` and `ToolAnnotations.Title`)
|
||||
- When `hints` are present, always emit annotations so write tools get
|
||||
`readOnlyHint: false` and `destructiveHint: false`, distinguishing them from
|
||||
deletes (`destructiveHint: true`)
|
||||
|
||||
`go.mod` replaces `go.probo.inc/mcpgen` with this directory. Once the same
|
||||
changes land upstream, drop the replace and delete this tree.
|
||||
21
third_party/mcpgen/go.mod
vendored
21
third_party/mcpgen/go.mod
vendored
@@ -1,21 +0,0 @@
|
||||
module go.probo.inc/mcpgen
|
||||
|
||||
go 1.25.3
|
||||
|
||||
require (
|
||||
github.com/google/jsonschema-go v0.3.0
|
||||
github.com/modelcontextprotocol/go-sdk v1.1.0
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/stretchr/testify v1.11.1
|
||||
golang.org/x/mod v0.30.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/spf13/pflag v1.0.9 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
)
|
||||
32
third_party/mcpgen/go.sum
vendored
32
third_party/mcpgen/go.sum
vendored
@@ -1,32 +0,0 @@
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
|
||||
github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/modelcontextprotocol/go-sdk v1.1.0 h1:Qjayg53dnKC4UZ+792W21e4BpwEZBzwgRW6LrjLWSwA=
|
||||
github.com/modelcontextprotocol/go-sdk v1.1.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
1292
third_party/mcpgen/internal/codegen/generator.go
vendored
1292
third_party/mcpgen/internal/codegen/generator.go
vendored
File diff suppressed because it is too large
Load Diff
1387
third_party/mcpgen/internal/codegen/generator_test.go
vendored
1387
third_party/mcpgen/internal/codegen/generator_test.go
vendored
File diff suppressed because it is too large
Load Diff
@@ -1,282 +0,0 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.probo.inc/mcpgen/internal/config"
|
||||
)
|
||||
|
||||
func TestGenerateWithCustomTypes(t *testing.T) {
|
||||
specPath := filepath.Join("testdata", "custom_types.yaml")
|
||||
spec, err := config.LoadMCPSpec(specPath)
|
||||
require.NoError(t, err, "Failed to load spec")
|
||||
|
||||
cfg := &config.Config{
|
||||
Spec: specPath,
|
||||
Output: t.TempDir(),
|
||||
Model: config.ModelConfig{
|
||||
Package: "test",
|
||||
Filename: "models.go",
|
||||
},
|
||||
Resolver: config.ResolverConfig{
|
||||
Package: "test",
|
||||
Filename: "resolver.go",
|
||||
Type: "Resolver",
|
||||
Preserve: false,
|
||||
},
|
||||
// No custom models in config - using go.probo.inc/mcpgen/type annotations
|
||||
Models: config.ModelsConfig{
|
||||
Models: map[string]config.TypeMapping{},
|
||||
},
|
||||
}
|
||||
|
||||
gen := New(cfg, spec)
|
||||
|
||||
if err := gen.loadSchemas(); err != nil {
|
||||
t.Fatalf("Failed to load schemas: %v", err)
|
||||
}
|
||||
|
||||
code, err := gen.typeGen.Generate("test")
|
||||
require.NoError(t, err, "Failed to generate code")
|
||||
|
||||
codeStr := string(code)
|
||||
|
||||
customTypes := []string{"Timestamp", "UUID", "Decimal", "Metadata", "Duration"}
|
||||
for _, typeName := range customTypes {
|
||||
assert.NotContains(t, codeStr, "type "+typeName+" ")
|
||||
}
|
||||
|
||||
regularTypes := []string{"Task", "OptionalFields", "Project", "UpdateTaskInput"}
|
||||
for _, typeName := range regularTypes {
|
||||
assert.Contains(t, codeStr, "type "+typeName)
|
||||
}
|
||||
|
||||
expectedImports := []string{
|
||||
"time",
|
||||
"github.com/google/uuid",
|
||||
"github.com/shopspring/decimal",
|
||||
"json",
|
||||
"go.probo.inc/mcpgen/mcp",
|
||||
}
|
||||
for _, imp := range expectedImports {
|
||||
assert.Contains(t, codeStr, `"`+imp+`"`)
|
||||
}
|
||||
|
||||
assert.Contains(t, codeStr, "ID uuid.UUID")
|
||||
assert.Contains(t, codeStr, "CreatedAt time.Time")
|
||||
assert.Contains(t, codeStr, "UpdatedAt *time.Time")
|
||||
assert.Contains(t, codeStr, "mcp.Omittable[*string]")
|
||||
assert.Contains(t, codeStr, "mcp.Omittable[*Status]")
|
||||
assert.Contains(t, codeStr, "mcp.Omittable[*int]")
|
||||
assert.Contains(t, codeStr, "mcp.Omittable[*[]string]")
|
||||
}
|
||||
|
||||
func TestGenerateWithConfigBasedTypes(t *testing.T) {
|
||||
specPath := filepath.Join("testdata", "config_based_types.yaml")
|
||||
spec, err := config.LoadMCPSpec(specPath)
|
||||
require.NoError(t, err, "Failed to load spec")
|
||||
|
||||
cfg := &config.Config{
|
||||
Spec: specPath,
|
||||
Output: t.TempDir(),
|
||||
Model: config.ModelConfig{
|
||||
Package: "test",
|
||||
Filename: "models.go",
|
||||
},
|
||||
Resolver: config.ResolverConfig{
|
||||
Package: "test",
|
||||
Filename: "resolver.go",
|
||||
Type: "Resolver",
|
||||
Preserve: false,
|
||||
},
|
||||
// Custom models in config
|
||||
Models: config.ModelsConfig{
|
||||
Models: map[string]config.TypeMapping{
|
||||
"Timestamp": {Model: "time.Time"},
|
||||
"UUID": {Model: "github.com/google/uuid.UUID"},
|
||||
"User": {Model: "github.com/myapp/models.User"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gen := New(cfg, spec)
|
||||
|
||||
if err := gen.loadSchemas(); err != nil {
|
||||
t.Fatalf("Failed to load schemas: %v", err)
|
||||
}
|
||||
|
||||
code, err := gen.typeGen.Generate("test")
|
||||
require.NoError(t, err, "Failed to generate code")
|
||||
|
||||
codeStr := string(code)
|
||||
|
||||
customTypes := []string{"Timestamp", "UUID", "User"}
|
||||
for _, typeName := range customTypes {
|
||||
assert.NotContains(t, codeStr, "type "+typeName+" ")
|
||||
}
|
||||
|
||||
assert.Contains(t, codeStr, "type Event struct")
|
||||
|
||||
expectedImports := []string{
|
||||
"time",
|
||||
"github.com/google/uuid",
|
||||
"github.com/myapp/models",
|
||||
}
|
||||
for _, imp := range expectedImports {
|
||||
assert.Contains(t, codeStr, `"`+imp+`"`)
|
||||
}
|
||||
|
||||
assert.Contains(t, codeStr, "ID uuid.UUID")
|
||||
assert.Contains(t, codeStr, "CreatedAt time.Time")
|
||||
assert.Contains(t, codeStr, "Owner *models.User")
|
||||
}
|
||||
|
||||
func TestGenerateAllPrimitives(t *testing.T) {
|
||||
specPath := filepath.Join("testdata", "all_primitives.yaml")
|
||||
spec, err := config.LoadMCPSpec(specPath)
|
||||
require.NoError(t, err, "Failed to load spec")
|
||||
|
||||
cfg := &config.Config{
|
||||
Spec: specPath,
|
||||
Output: t.TempDir(),
|
||||
Model: config.ModelConfig{
|
||||
Package: "test",
|
||||
Filename: "models.go",
|
||||
},
|
||||
Resolver: config.ResolverConfig{
|
||||
Package: "test",
|
||||
Filename: "resolver.go",
|
||||
Type: "Resolver",
|
||||
Preserve: false,
|
||||
},
|
||||
Models: config.ModelsConfig{
|
||||
Models: map[string]config.TypeMapping{},
|
||||
},
|
||||
}
|
||||
|
||||
gen := New(cfg, spec)
|
||||
|
||||
if err := gen.loadSchemas(); err != nil {
|
||||
t.Fatalf("Failed to load schemas: %v", err)
|
||||
}
|
||||
|
||||
code, err := gen.typeGen.Generate("test")
|
||||
require.NoError(t, err, "Failed to generate code")
|
||||
|
||||
codeStr := string(code)
|
||||
|
||||
primitiveTypes := map[string]string{
|
||||
"StringSchema": "type StringSchema string",
|
||||
"NumberSchema": "type NumberSchema float64",
|
||||
"IntegerSchema": "type IntegerSchema int",
|
||||
"BooleanSchema": "type BooleanSchema bool",
|
||||
"ArraySchema": "type ArraySchema []string",
|
||||
}
|
||||
|
||||
for typeName, expectedDecl := range primitiveTypes {
|
||||
if !containsString(codeStr, expectedDecl) {
|
||||
t.Errorf("Should generate %q for %s", expectedDecl, typeName)
|
||||
}
|
||||
}
|
||||
|
||||
if !containsString(codeStr, "type ObjectSchema struct") {
|
||||
t.Error("Should generate ObjectSchema as a struct")
|
||||
}
|
||||
|
||||
if !containsString(codeStr, "type Person struct") {
|
||||
t.Error("Should generate Person as a struct")
|
||||
}
|
||||
|
||||
if !containsString(codeStr, "type Color string") {
|
||||
t.Error("Should generate Color as string-based enum")
|
||||
}
|
||||
|
||||
enumConstants := []string{"ColorRed", "ColorGreen", "ColorBlue", "ColorYellow"}
|
||||
for _, constName := range enumConstants {
|
||||
if !containsString(codeStr, constName) {
|
||||
t.Errorf("Should generate enum constant %q", constName)
|
||||
}
|
||||
}
|
||||
|
||||
if len(code) == 0 {
|
||||
t.Error("Generated code is empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratedCodeCompiles(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
specFile string
|
||||
config *config.Config
|
||||
}{
|
||||
{
|
||||
name: "custom_types",
|
||||
specFile: "custom_types.yaml",
|
||||
config: &config.Config{
|
||||
Model: config.ModelConfig{
|
||||
Package: "test",
|
||||
Filename: "models.go",
|
||||
},
|
||||
Models: config.ModelsConfig{
|
||||
Models: map[string]config.TypeMapping{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "config_based_types",
|
||||
specFile: "config_based_types.yaml",
|
||||
config: &config.Config{
|
||||
Model: config.ModelConfig{
|
||||
Package: "test",
|
||||
Filename: "models.go",
|
||||
},
|
||||
Models: config.ModelsConfig{
|
||||
Models: map[string]config.TypeMapping{
|
||||
"Timestamp": {Model: "time.Time"},
|
||||
"UUID": {Model: "github.com/google/uuid.UUID"},
|
||||
"User": {Model: "github.com/myapp/models.User"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
specPath := filepath.Join("testdata", tc.specFile)
|
||||
spec, err := config.LoadMCPSpec(specPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load spec: %v", err)
|
||||
}
|
||||
|
||||
tc.config.Spec = specPath
|
||||
tc.config.Output = t.TempDir()
|
||||
tc.config.Resolver = config.ResolverConfig{
|
||||
Package: "test",
|
||||
Filename: "resolver.go",
|
||||
Type: "Resolver",
|
||||
Preserve: false,
|
||||
}
|
||||
|
||||
gen := New(tc.config, spec)
|
||||
if err := gen.loadSchemas(); err != nil {
|
||||
t.Fatalf("Failed to load schemas: %v", err)
|
||||
}
|
||||
|
||||
code, err := gen.typeGen.Generate("test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate code: %v", err)
|
||||
}
|
||||
|
||||
// The fact that Generate() succeeded means the code was formatted successfully
|
||||
if len(code) == 0 {
|
||||
t.Error("Generated code is empty")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
191
third_party/mcpgen/internal/codegen/parser.go
vendored
191
third_party/mcpgen/internal/codegen/parser.go
vendored
@@ -1,191 +0,0 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/printer"
|
||||
"go/token"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type HandlerInfo struct {
|
||||
Name string
|
||||
RecvType string
|
||||
SourceCode string
|
||||
IsOrphaned bool
|
||||
}
|
||||
|
||||
type ResolverParser struct {
|
||||
filePath string
|
||||
fset *token.FileSet
|
||||
file *ast.File
|
||||
}
|
||||
|
||||
func NewResolverParser(filePath string) (*ResolverParser, error) {
|
||||
fset := token.NewFileSet()
|
||||
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("resolver file not found: %s", filePath)
|
||||
}
|
||||
|
||||
file, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse resolver file: %w", err)
|
||||
}
|
||||
|
||||
return &ResolverParser{
|
||||
filePath: filePath,
|
||||
fset: fset,
|
||||
file: file,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *ResolverParser) ExtractHandlers(resolverType string) (map[string]*HandlerInfo, error) {
|
||||
handlers := make(map[string]*HandlerInfo)
|
||||
|
||||
// Extract from both old wrapper types and new direct Resolver type
|
||||
allowedTypes := map[string]bool{
|
||||
// Old wrapper types (for backward compatibility during migration)
|
||||
"toolResolver": true,
|
||||
"*toolResolver": true,
|
||||
"promptResolver": true,
|
||||
"*promptResolver": true,
|
||||
"resourceResolver": true,
|
||||
"*resourceResolver": true,
|
||||
// New direct Resolver type
|
||||
resolverType: true,
|
||||
"*" + resolverType: true,
|
||||
}
|
||||
|
||||
for _, decl := range p.file.Decls {
|
||||
funcDecl, ok := decl.(*ast.FuncDecl)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if funcDecl.Recv == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
recvType := p.getReceiverType(funcDecl.Recv)
|
||||
if !allowedTypes[recvType] {
|
||||
continue
|
||||
}
|
||||
|
||||
methodName := funcDecl.Name.Name
|
||||
sourceCode, err := p.extractFunctionSource(funcDecl)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract source for %s: %w", methodName, err)
|
||||
}
|
||||
|
||||
// Transform receiver type from old wrapper types to main Resolver type
|
||||
sourceCode = TransformReceiverType(sourceCode, resolverType)
|
||||
|
||||
handlers[methodName] = &HandlerInfo{
|
||||
Name: methodName,
|
||||
RecvType: "*" + resolverType, // Always use main Resolver type
|
||||
SourceCode: sourceCode,
|
||||
IsOrphaned: false,
|
||||
}
|
||||
}
|
||||
|
||||
return handlers, nil
|
||||
}
|
||||
|
||||
func (p *ResolverParser) getReceiverType(recv *ast.FieldList) string {
|
||||
if recv == nil || len(recv.List) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
field := recv.List[0]
|
||||
switch typ := field.Type.(type) {
|
||||
case *ast.Ident:
|
||||
return typ.Name
|
||||
case *ast.StarExpr:
|
||||
if ident, ok := typ.X.(*ast.Ident); ok {
|
||||
return "*" + ident.Name
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *ResolverParser) extractFunctionSource(funcDecl *ast.FuncDecl) (string, error) {
|
||||
var buf strings.Builder
|
||||
|
||||
cfg := printer.Config{
|
||||
Mode: printer.UseSpaces | printer.TabIndent,
|
||||
Tabwidth: 8,
|
||||
}
|
||||
|
||||
if err := cfg.Fprint(&buf, p.fset, funcDecl); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// TransformReceiverType rewrites the receiver type in handler source code from old wrapper types
|
||||
// (toolResolver, promptResolver, resourceResolver) to the main Resolver type
|
||||
func TransformReceiverType(sourceCode, resolverType string) string {
|
||||
// Replace old wrapper types with main Resolver type
|
||||
sourceCode = strings.ReplaceAll(sourceCode, "*toolResolver)", "*"+resolverType+")")
|
||||
sourceCode = strings.ReplaceAll(sourceCode, "*promptResolver)", "*"+resolverType+")")
|
||||
sourceCode = strings.ReplaceAll(sourceCode, "*resourceResolver)", "*"+resolverType+")")
|
||||
return sourceCode
|
||||
}
|
||||
|
||||
func IdentifyOrphanedHandlers(existingHandlers map[string]*HandlerInfo, requiredHandlers []string) {
|
||||
requiredSet := make(map[string]bool)
|
||||
for _, name := range requiredHandlers {
|
||||
requiredSet[name] = true
|
||||
}
|
||||
|
||||
for name, handler := range existingHandlers {
|
||||
if !requiredSet[name] {
|
||||
handler.IsOrphaned = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func FormatOrphanedHandlers(handlers map[string]*HandlerInfo) string {
|
||||
var orphaned []*HandlerInfo
|
||||
|
||||
for _, handler := range handlers {
|
||||
if handler.IsOrphaned {
|
||||
orphaned = append(orphaned, handler)
|
||||
}
|
||||
}
|
||||
|
||||
if len(orphaned) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var buf strings.Builder
|
||||
buf.WriteString("\n\n// ==============================================================================\n")
|
||||
buf.WriteString("// Orphaned Handlers\n")
|
||||
buf.WriteString("// ==============================================================================\n")
|
||||
buf.WriteString("// The following handlers were found in the resolver file but are no longer\n")
|
||||
buf.WriteString("// defined in the MCP specification. They have been preserved here as comments\n")
|
||||
buf.WriteString("// in case you need to reference or restore them.\n")
|
||||
buf.WriteString("// ==============================================================================\n\n")
|
||||
|
||||
for _, handler := range orphaned {
|
||||
buf.WriteString(fmt.Sprintf("// Orphaned: %s\n", handler.Name))
|
||||
buf.WriteString("// Uncomment and update signature if you want to restore this handler.\n")
|
||||
|
||||
lines := strings.Split(handler.SourceCode, "\n")
|
||||
for _, line := range lines {
|
||||
if strings.TrimSpace(line) != "" {
|
||||
buf.WriteString("// ")
|
||||
}
|
||||
buf.WriteString(line)
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
484
third_party/mcpgen/internal/codegen/parser_test.go
vendored
484
third_party/mcpgen/internal/codegen/parser_test.go
vendored
@@ -1,484 +0,0 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewResolverParser(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
content string
|
||||
wantErr bool
|
||||
setupFile bool
|
||||
}{
|
||||
{
|
||||
name: "valid resolver file",
|
||||
content: `package test
|
||||
|
||||
type Resolver struct{}
|
||||
|
||||
func (r *Resolver) GetUser(ctx context.Context) error {
|
||||
return nil
|
||||
}`,
|
||||
setupFile: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "non-existent file",
|
||||
content: "",
|
||||
setupFile: false,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid Go syntax",
|
||||
content: `package test
|
||||
func ( { // invalid syntax
|
||||
}`,
|
||||
setupFile: true,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var testFile string
|
||||
if tt.setupFile {
|
||||
tmpDir := t.TempDir()
|
||||
testFile = filepath.Join(tmpDir, "resolver.go")
|
||||
if err := os.WriteFile(testFile, []byte(tt.content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test file: %v", err)
|
||||
}
|
||||
} else {
|
||||
testFile = filepath.Join(t.TempDir(), "nonexistent.go")
|
||||
}
|
||||
|
||||
parser, err := NewResolverParser(testFile)
|
||||
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Error("Expected error but got nil")
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if parser == nil {
|
||||
t.Error("Expected parser but got nil")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractHandlers(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
content string
|
||||
resolverType string
|
||||
wantHandlers []string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "extract toolResolver handlers",
|
||||
content: `package test
|
||||
|
||||
type toolResolver struct{}
|
||||
|
||||
func (r *toolResolver) HandleListTasks(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *toolResolver) HandleCreateTask(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Not a handler - no receiver
|
||||
func HelperFunction() {}
|
||||
`,
|
||||
resolverType: "Resolver",
|
||||
wantHandlers: []string{"HandleListTasks", "HandleCreateTask"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "extract promptResolver handlers",
|
||||
content: `package test
|
||||
|
||||
type promptResolver struct{}
|
||||
|
||||
func (r *promptResolver) HandleGetPrompt(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
`,
|
||||
resolverType: "Resolver",
|
||||
wantHandlers: []string{"HandleGetPrompt"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "extract resourceResolver handlers",
|
||||
content: `package test
|
||||
|
||||
type resourceResolver struct{}
|
||||
|
||||
func (r *resourceResolver) HandleReadResource(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
`,
|
||||
resolverType: "Resolver",
|
||||
wantHandlers: []string{"HandleReadResource"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "mixed resolver types",
|
||||
content: `package test
|
||||
|
||||
type toolResolver struct{}
|
||||
type promptResolver struct{}
|
||||
type resourceResolver struct{}
|
||||
|
||||
func (r *toolResolver) HandleTool(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *promptResolver) HandlePrompt(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *resourceResolver) HandleResource(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type OtherType struct{}
|
||||
|
||||
func (r *OtherType) NotAHandler(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
`,
|
||||
resolverType: "Resolver",
|
||||
wantHandlers: []string{"HandleTool", "HandlePrompt", "HandleResource"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "no handlers",
|
||||
content: `package test
|
||||
|
||||
type Resolver struct{}
|
||||
|
||||
func HelperFunction() {}
|
||||
`,
|
||||
resolverType: "Resolver",
|
||||
wantHandlers: []string{},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "pointer and value receivers",
|
||||
content: `package test
|
||||
|
||||
type toolResolver struct{}
|
||||
|
||||
func (r toolResolver) HandleNonPointer(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *toolResolver) HandlePointer(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
`,
|
||||
resolverType: "Resolver",
|
||||
wantHandlers: []string{"HandleNonPointer", "HandlePointer"}, // Both are accepted
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "resolver.go")
|
||||
if err := os.WriteFile(testFile, []byte(tt.content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test file: %v", err)
|
||||
}
|
||||
|
||||
parser, err := NewResolverParser(testFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create parser: %v", err)
|
||||
}
|
||||
|
||||
handlers, err := parser.ExtractHandlers(tt.resolverType)
|
||||
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Error("Expected error but got nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(handlers) != len(tt.wantHandlers) {
|
||||
t.Errorf("Expected %d handlers, got %d", len(tt.wantHandlers), len(handlers))
|
||||
}
|
||||
|
||||
for _, wantName := range tt.wantHandlers {
|
||||
if _, ok := handlers[wantName]; !ok {
|
||||
t.Errorf("Expected handler %q not found", wantName)
|
||||
}
|
||||
}
|
||||
|
||||
for name, handler := range handlers {
|
||||
if handler.Name != name {
|
||||
t.Errorf("Handler name mismatch: got %q, want %q", handler.Name, name)
|
||||
}
|
||||
if handler.SourceCode == "" {
|
||||
t.Errorf("Handler %q has empty source code", name)
|
||||
}
|
||||
if !strings.Contains(handler.RecvType, "Resolver") {
|
||||
t.Errorf("Handler %q has unexpected receiver type: %q", name, handler.RecvType)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentifyOrphanedHandlers(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
existingHandlers map[string]*HandlerInfo
|
||||
requiredHandlers []string
|
||||
wantOrphaned []string
|
||||
}{
|
||||
{
|
||||
name: "no orphaned handlers",
|
||||
existingHandlers: map[string]*HandlerInfo{
|
||||
"HandleA": {Name: "HandleA"},
|
||||
"HandleB": {Name: "HandleB"},
|
||||
},
|
||||
requiredHandlers: []string{"HandleA", "HandleB"},
|
||||
wantOrphaned: []string{},
|
||||
},
|
||||
{
|
||||
name: "one orphaned handler",
|
||||
existingHandlers: map[string]*HandlerInfo{
|
||||
"HandleA": {Name: "HandleA"},
|
||||
"HandleB": {Name: "HandleB"},
|
||||
"HandleC": {Name: "HandleC"},
|
||||
},
|
||||
requiredHandlers: []string{"HandleA", "HandleB"},
|
||||
wantOrphaned: []string{"HandleC"},
|
||||
},
|
||||
{
|
||||
name: "all orphaned",
|
||||
existingHandlers: map[string]*HandlerInfo{
|
||||
"HandleA": {Name: "HandleA"},
|
||||
"HandleB": {Name: "HandleB"},
|
||||
},
|
||||
requiredHandlers: []string{},
|
||||
wantOrphaned: []string{"HandleA", "HandleB"},
|
||||
},
|
||||
{
|
||||
name: "new handlers required",
|
||||
existingHandlers: map[string]*HandlerInfo{
|
||||
"HandleA": {Name: "HandleA"},
|
||||
},
|
||||
requiredHandlers: []string{"HandleA", "HandleB", "HandleC"},
|
||||
wantOrphaned: []string{},
|
||||
},
|
||||
{
|
||||
name: "empty existing handlers",
|
||||
existingHandlers: map[string]*HandlerInfo{},
|
||||
requiredHandlers: []string{"HandleA"},
|
||||
wantOrphaned: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
IdentifyOrphanedHandlers(tt.existingHandlers, tt.requiredHandlers)
|
||||
|
||||
var gotOrphaned []string
|
||||
for name, handler := range tt.existingHandlers {
|
||||
if handler.IsOrphaned {
|
||||
gotOrphaned = append(gotOrphaned, name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(gotOrphaned) != len(tt.wantOrphaned) {
|
||||
t.Errorf("Expected %d orphaned handlers, got %d", len(tt.wantOrphaned), len(gotOrphaned))
|
||||
}
|
||||
|
||||
orphanedSet := make(map[string]bool)
|
||||
for _, name := range gotOrphaned {
|
||||
orphanedSet[name] = true
|
||||
}
|
||||
|
||||
for _, wantName := range tt.wantOrphaned {
|
||||
if !orphanedSet[wantName] {
|
||||
t.Errorf("Expected %q to be orphaned but it wasn't", wantName)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatOrphanedHandlers(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
handlers map[string]*HandlerInfo
|
||||
wantContains []string
|
||||
isEmpty bool
|
||||
}{
|
||||
{
|
||||
name: "single orphaned handler",
|
||||
handlers: map[string]*HandlerInfo{
|
||||
"HandleOldTask": {
|
||||
Name: "HandleOldTask",
|
||||
IsOrphaned: true,
|
||||
SourceCode: `func (r *Resolver) HandleOldTask(ctx context.Context) error {
|
||||
return nil
|
||||
}`,
|
||||
},
|
||||
},
|
||||
wantContains: []string{
|
||||
"Orphaned Handlers",
|
||||
"Orphaned: HandleOldTask",
|
||||
"// func (r *Resolver) HandleOldTask",
|
||||
},
|
||||
isEmpty: false,
|
||||
},
|
||||
{
|
||||
name: "multiple orphaned handlers",
|
||||
handlers: map[string]*HandlerInfo{
|
||||
"HandleA": {
|
||||
Name: "HandleA",
|
||||
IsOrphaned: true,
|
||||
SourceCode: "func (r *Resolver) HandleA() {}",
|
||||
},
|
||||
"HandleB": {
|
||||
Name: "HandleB",
|
||||
IsOrphaned: true,
|
||||
SourceCode: "func (r *Resolver) HandleB() {}",
|
||||
},
|
||||
},
|
||||
wantContains: []string{
|
||||
"Orphaned: HandleA",
|
||||
"Orphaned: HandleB",
|
||||
},
|
||||
isEmpty: false,
|
||||
},
|
||||
{
|
||||
name: "no orphaned handlers",
|
||||
handlers: map[string]*HandlerInfo{
|
||||
"HandleActive": {
|
||||
Name: "HandleActive",
|
||||
IsOrphaned: false,
|
||||
SourceCode: "func (r *Resolver) HandleActive() {}",
|
||||
},
|
||||
},
|
||||
wantContains: []string{},
|
||||
isEmpty: true,
|
||||
},
|
||||
{
|
||||
name: "empty handlers map",
|
||||
handlers: map[string]*HandlerInfo{},
|
||||
wantContains: []string{},
|
||||
isEmpty: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := FormatOrphanedHandlers(tt.handlers)
|
||||
|
||||
if tt.isEmpty {
|
||||
if result != "" {
|
||||
t.Errorf("Expected empty result, got: %q", result)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if result == "" {
|
||||
t.Error("Expected non-empty result but got empty string")
|
||||
return
|
||||
}
|
||||
|
||||
for _, want := range tt.wantContains {
|
||||
if !strings.Contains(result, want) {
|
||||
t.Errorf("Result should contain %q but doesn't.\nGot: %s", want, result)
|
||||
}
|
||||
}
|
||||
|
||||
assert.Contains(t, result, "Orphaned Handlers", "Result should contain orphaned handlers header")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetReceiverType(t *testing.T) {
|
||||
// but we can add a direct test using a sample AST if needed
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
|
||||
content := `package test
|
||||
|
||||
type toolResolver struct{}
|
||||
|
||||
func (r *toolResolver) Method() {}
|
||||
`
|
||||
if err := os.WriteFile(testFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test file: %v", err)
|
||||
}
|
||||
|
||||
parser, err := NewResolverParser(testFile)
|
||||
require.NoError(t, err, "Failed to create parser")
|
||||
|
||||
handlers, err := parser.ExtractHandlers("Resolver")
|
||||
require.NoError(t, err, "Failed to extract handlers")
|
||||
|
||||
if len(handlers) != 1 {
|
||||
t.Fatalf("Expected 1 handler, got %d", len(handlers))
|
||||
}
|
||||
|
||||
handler := handlers["Method"]
|
||||
// After transformation, the receiver type should be *Resolver
|
||||
if handler.RecvType != "*Resolver" {
|
||||
t.Errorf("Expected receiver type '*Resolver' (after transformation), got %q", handler.RecvType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractFunctionSource(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
|
||||
content := `package test
|
||||
|
||||
type toolResolver struct{}
|
||||
|
||||
func (r *toolResolver) HandleTest(ctx context.Context) error {
|
||||
x := 42
|
||||
return nil
|
||||
}
|
||||
`
|
||||
if err := os.WriteFile(testFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test file: %v", err)
|
||||
}
|
||||
|
||||
parser, err := NewResolverParser(testFile)
|
||||
require.NoError(t, err, "Failed to create parser")
|
||||
|
||||
handlers, err := parser.ExtractHandlers("Resolver")
|
||||
require.NoError(t, err, "Failed to extract handlers")
|
||||
|
||||
handler := handlers["HandleTest"]
|
||||
// After transformation, toolResolver should be changed to Resolver
|
||||
if !strings.Contains(handler.SourceCode, "func (r *Resolver) HandleTest") {
|
||||
t.Errorf("Source code should contain transformed function signature, got: %s", handler.SourceCode)
|
||||
}
|
||||
assert.Contains(t, handler.SourceCode, "return nil", "Source code should contain function body")
|
||||
assert.Contains(t, handler.SourceCode, "x := 42", "Source code should contain function body statements")
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package {{.Package}}
|
||||
|
||||
// This file will be automatically regenerated based on the schema, any resolver implementations
|
||||
// will be copied through when generating and any unknown code will be moved to the end.
|
||||
// Code generated by mcpgen. DO NOT EDIT.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
{{- if .Imports}}
|
||||
{{- range .Imports}}
|
||||
{{- if .Alias}}
|
||||
{{.Alias}} "{{.Path}}"
|
||||
{{- else}}
|
||||
"{{.Path}}"
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
)
|
||||
|
||||
{{- range .Tools}}
|
||||
|
||||
{{- if .HasInputType}}
|
||||
func (r *{{$.ResolverType}}) {{.HandlerName}}Tool(ctx context.Context, req *mcp.CallToolRequest, input *{{.InputType}}) (*mcp.CallToolResult, {{if .HasOutputType}}{{.OutputType}}{{else}}map[string]any{{end}}, error) {
|
||||
return nil, {{if .HasOutputType}}{{.OutputType}}{}{{else}}nil{{end}}, fmt.Errorf("{{.Name}} not implemented")
|
||||
}
|
||||
{{- else}}
|
||||
func (r *{{$.ResolverType}}) {{.HandlerName}}Tool(ctx context.Context, req *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, {{if .HasOutputType}}{{.OutputType}}{{else}}map[string]any{{end}}, error) {
|
||||
return nil, {{if .HasOutputType}}{{.OutputType}}{}{{else}}nil{{end}}, fmt.Errorf("{{.Name}} not implemented")
|
||||
}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
|
||||
{{- if .HasResources}}
|
||||
{{- range .Resources}}
|
||||
|
||||
func (r *{{$.ResolverType}}) {{.HandlerName}}Resource(ctx context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
|
||||
return nil, fmt.Errorf("{{.Name}} not implemented")
|
||||
}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
|
||||
{{- if .HasPrompts}}
|
||||
{{- range .Prompts}}
|
||||
|
||||
{{- if .HasArgsType}}
|
||||
func (r *{{$.ResolverType}}) {{.HandlerName}}Prompt(ctx context.Context, req *mcp.GetPromptRequest, args {{.ArgsType}}) (*mcp.GetPromptResult, error) {
|
||||
return nil, fmt.Errorf("{{.Name}} not implemented")
|
||||
}
|
||||
{{- else}}
|
||||
func (r *{{$.ResolverType}}) {{.HandlerName}}Prompt(ctx context.Context, req *mcp.GetPromptRequest, args map[string]string) (*mcp.GetPromptResult, error) {
|
||||
return nil, fmt.Errorf("{{.Name}} not implemented")
|
||||
}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
@@ -1,22 +0,0 @@
|
||||
package {{.Package}}
|
||||
|
||||
// This file will NOT be regenerated automatically.
|
||||
//
|
||||
// It serves as a dependency injection container for your resolvers.
|
||||
// Add any dependencies you need here (database connections, API clients, etc.)
|
||||
// and they'll be available to all your tool, prompt, and resource resolvers.
|
||||
|
||||
// {{.ResolverType}} is the root resolver that holds dependencies for all MCP handlers
|
||||
type {{.ResolverType}} struct {
|
||||
// Add your dependencies here, for example:
|
||||
// DB *sql.DB
|
||||
// Cache *redis.Client
|
||||
// APIClient *http.Client
|
||||
}
|
||||
|
||||
// New{{.ResolverType}} creates a new resolver instance
|
||||
func New{{.ResolverType}}() *{{.ResolverType}} {
|
||||
return &{{.ResolverType}}{
|
||||
// Initialize your dependencies here
|
||||
}
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
// Code generated by mcpgen. DO NOT EDIT.
|
||||
|
||||
package {{.Package}}
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
{{- if .Imports}}
|
||||
{{- range .Imports}}
|
||||
{{- if .Alias}}
|
||||
{{.Alias}} "{{.Path}}"
|
||||
{{- else}}
|
||||
"{{.Path}}"
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
mcputil "go.probo.inc/mcpgen/mcp"
|
||||
)
|
||||
|
||||
// ResolverInterface defines the interface that must be implemented by the parent resolver
|
||||
type ResolverInterface interface {
|
||||
{{- range .Tools}}
|
||||
{{.HandlerName}}Tool(ctx context.Context, req *mcp.CallToolRequest{{if .HasInputType}}, input *{{.InputType}}{{else}}, args map[string]any{{end}}) (*mcp.CallToolResult, {{if .HasOutputType}}{{.OutputType}}{{else}}map[string]any{{end}}, error)
|
||||
{{- end}}
|
||||
{{- if .HasResources}}
|
||||
{{- range .Resources}}
|
||||
{{.HandlerName}}Resource(ctx context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error)
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
{{- if .HasPrompts}}
|
||||
{{- range .Prompts}}
|
||||
{{.HandlerName}}Prompt(ctx context.Context, req *mcp.GetPromptRequest{{if .HasArgsType}}, args {{.ArgsType}}{{else}}, args map[string]string{{end}}) (*mcp.GetPromptResult, error)
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
}
|
||||
|
||||
// New creates a new MCP server instance with all handlers registered.
|
||||
// Returns a fully configured *mcp.Server ready to be used with any transport.
|
||||
func New(resolver ResolverInterface, opts ...mcputil.Option) *mcp.Server {
|
||||
o := mcputil.ApplyOptions(opts)
|
||||
|
||||
server := mcp.NewServer(
|
||||
&mcp.Implementation{
|
||||
Name: "{{.ServerName}}",
|
||||
Version: "{{.ServerVersion}}",
|
||||
},
|
||||
nil,
|
||||
)
|
||||
|
||||
registerToolHandlers(server, resolver, &o)
|
||||
{{- if .HasResources}}
|
||||
registerResourceHandlers(server, resolver)
|
||||
{{- end}}
|
||||
{{- if .HasPrompts}}
|
||||
registerPromptHandlers(server, resolver)
|
||||
{{- end}}
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
func registerToolHandlers(server *mcp.Server, resolver ResolverInterface, opts *mcputil.Options) {
|
||||
{{- range .Tools}}
|
||||
{{- $hasAnnotations := or .HasHints .Title}}
|
||||
mcp.AddTool(
|
||||
server,
|
||||
&mcp.Tool{
|
||||
Name: "{{.Name}}",
|
||||
{{- if .Title}}
|
||||
Title: "{{.Title}}",
|
||||
{{- end}}
|
||||
Description: "{{.Description}}",
|
||||
{{- if .HasInputType}}
|
||||
InputSchema: {{.InputSchemaVar}},
|
||||
{{- end}}
|
||||
{{- if .HasOutputType}}
|
||||
OutputSchema: {{.OutputSchemaVar}},
|
||||
{{- end}}
|
||||
{{- if $hasAnnotations}}
|
||||
Annotations: &mcp.ToolAnnotations{
|
||||
{{- if .Title}}
|
||||
Title: "{{.Title}}",
|
||||
{{- end}}
|
||||
{{- if .Readonly}}
|
||||
ReadOnlyHint: true,
|
||||
{{- else if .HasHints}}
|
||||
ReadOnlyHint: false,
|
||||
DestructiveHint: boolPtr({{if .Destructive}}true{{else}}false{{end}}),
|
||||
{{- end}}
|
||||
{{- if .Idempotent}}
|
||||
IdempotentHint: true,
|
||||
{{- end}}
|
||||
{{- if .OpenWorld}}
|
||||
OpenWorldHint: boolPtr(true),
|
||||
{{- end}}
|
||||
},
|
||||
{{- end}}
|
||||
},
|
||||
func(ctx context.Context, req *mcp.CallToolRequest, input {{if .HasInputType}}*{{.InputType}}{{else}}map[string]any{{end}}) (result *mcp.CallToolResult, output {{if .HasOutputType}}{{.OutputType}}{{else}}map[string]any{{end}}, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = opts.RecoverFunc(ctx, r)
|
||||
}
|
||||
}()
|
||||
return resolver.{{.HandlerName}}Tool(ctx, req, input)
|
||||
},
|
||||
)
|
||||
|
||||
{{- end}}
|
||||
}
|
||||
|
||||
func boolPtr(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
|
||||
{{- if .HasResources}}
|
||||
|
||||
func registerResourceHandlers(server *mcp.Server, resolver ResolverInterface) {
|
||||
{{- range .Resources}}
|
||||
{{- if .URI}}
|
||||
server.AddResource(
|
||||
&mcp.Resource{
|
||||
URI: "{{.URI}}",
|
||||
Name: "{{.Name}}",
|
||||
Description: "{{.Description}}",
|
||||
{{- if .MimeType}}
|
||||
MIMEType: "{{.MimeType}}",
|
||||
{{- end}}
|
||||
},
|
||||
resolver.{{.HandlerName}}Resource,
|
||||
)
|
||||
|
||||
{{- else if .URITemplate}}
|
||||
server.AddResourceTemplate(
|
||||
&mcp.ResourceTemplate{
|
||||
URITemplate: "{{.URITemplate}}",
|
||||
Name: "{{.Name}}",
|
||||
Description: "{{.Description}}",
|
||||
{{- if .MimeType}}
|
||||
MIMEType: "{{.MimeType}}",
|
||||
{{- end}}
|
||||
},
|
||||
resolver.{{.HandlerName}}Resource,
|
||||
)
|
||||
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
}
|
||||
{{- end}}
|
||||
|
||||
{{- if .HasPrompts}}
|
||||
|
||||
func registerPromptHandlers(server *mcp.Server, resolver ResolverInterface) {
|
||||
{{- range .Prompts}}
|
||||
mcputil.AddPrompt(
|
||||
server,
|
||||
&mcp.Prompt{
|
||||
Name: "{{.Name}}",
|
||||
Description: "{{.Description}}",
|
||||
{{- if .Arguments}}
|
||||
Arguments: []*mcp.PromptArgument{
|
||||
{{- range .Arguments}}
|
||||
{
|
||||
Name: "{{.Name}}",
|
||||
Description: "{{.Description}}",
|
||||
Required: {{.Required}},
|
||||
},
|
||||
{{- end}}
|
||||
},
|
||||
{{- end}}
|
||||
},
|
||||
resolver.{{.HandlerName}}Prompt,
|
||||
)
|
||||
{{- end}}
|
||||
}
|
||||
{{- end}}
|
||||
@@ -1,222 +0,0 @@
|
||||
info:
|
||||
title: all-primitives-test
|
||||
version: 1.0.0
|
||||
description: Test all MCP primitives and JSON Schema features
|
||||
|
||||
components:
|
||||
schemas:
|
||||
# All JSON Schema types
|
||||
StringSchema:
|
||||
type: string
|
||||
description: A string
|
||||
|
||||
NumberSchema:
|
||||
type: number
|
||||
description: A number
|
||||
|
||||
IntegerSchema:
|
||||
type: integer
|
||||
description: An integer
|
||||
|
||||
BooleanSchema:
|
||||
type: boolean
|
||||
description: A boolean
|
||||
|
||||
ArraySchema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: An array of strings
|
||||
|
||||
ObjectSchema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
value:
|
||||
type: number
|
||||
required: [name]
|
||||
|
||||
# Enum types
|
||||
Color:
|
||||
type: string
|
||||
enum: [red, green, blue, yellow]
|
||||
description: A color
|
||||
|
||||
# Nested objects
|
||||
Address:
|
||||
type: object
|
||||
properties:
|
||||
street:
|
||||
type: string
|
||||
city:
|
||||
type: string
|
||||
zipCode:
|
||||
type: string
|
||||
country:
|
||||
type: string
|
||||
required: [city, country]
|
||||
|
||||
Person:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
age:
|
||||
type: integer
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
address:
|
||||
$ref: "#/components/schemas/Address"
|
||||
favoriteColor:
|
||||
$ref: "#/components/schemas/Color"
|
||||
tags:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
required: [name]
|
||||
|
||||
# Nullable fields with anyOf
|
||||
NullableFields:
|
||||
type: object
|
||||
properties:
|
||||
nullableString:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
nullableNumber:
|
||||
anyOf:
|
||||
- type: number
|
||||
- type: "null"
|
||||
nullableObject:
|
||||
anyOf:
|
||||
- $ref: "#/components/schemas/Address"
|
||||
- type: "null"
|
||||
|
||||
# Complex nested structure
|
||||
Organization:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
members:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Person"
|
||||
headquarters:
|
||||
$ref: "#/components/schemas/Address"
|
||||
founded:
|
||||
type: string
|
||||
format: date
|
||||
required: [id, name]
|
||||
|
||||
tools:
|
||||
# Tool with inline schema
|
||||
- name: simple_tool
|
||||
description: A simple tool with inline schema
|
||||
inputSchema:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
count:
|
||||
type: integer
|
||||
required: [message]
|
||||
|
||||
# Tool with ref schema
|
||||
- name: create_person
|
||||
description: Create a person
|
||||
inputSchema:
|
||||
$ref: "#/components/schemas/Person"
|
||||
|
||||
# Tool with complex schema
|
||||
- name: create_organization
|
||||
description: Create an organization
|
||||
inputSchema:
|
||||
$ref: "#/components/schemas/Organization"
|
||||
|
||||
# Tool with enum
|
||||
- name: set_color
|
||||
description: Set a color
|
||||
inputSchema:
|
||||
type: object
|
||||
properties:
|
||||
color:
|
||||
$ref: "#/components/schemas/Color"
|
||||
required: [color]
|
||||
|
||||
# Tool with nullable fields
|
||||
- name: update_fields
|
||||
description: Update optional fields
|
||||
inputSchema:
|
||||
$ref: "#/components/schemas/NullableFields"
|
||||
|
||||
resources:
|
||||
# Static resource
|
||||
- uri: "docs://readme"
|
||||
name: README
|
||||
description: The README document
|
||||
mimeType: text/markdown
|
||||
readonly: true
|
||||
|
||||
# Resource with simple template
|
||||
- uriTemplate: "person://{id}"
|
||||
name: Person Resource
|
||||
description: Get a person by ID
|
||||
mimeType: application/json
|
||||
readonly: true
|
||||
schema:
|
||||
$ref: "#/components/schemas/Person"
|
||||
|
||||
# Resource with multiple parameters
|
||||
- uriTemplate: "org://{orgId}/member/{memberId}"
|
||||
name: Organization Member
|
||||
description: Get a member of an organization
|
||||
mimeType: application/json
|
||||
readonly: true
|
||||
schema:
|
||||
$ref: "#/components/schemas/Person"
|
||||
|
||||
# Resource with nested schema
|
||||
- uriTemplate: "org://{id}"
|
||||
name: Organization Resource
|
||||
description: Get an organization by ID
|
||||
mimeType: application/json
|
||||
schema:
|
||||
$ref: "#/components/schemas/Organization"
|
||||
|
||||
prompts:
|
||||
# Prompt without arguments
|
||||
- name: help
|
||||
description: Get general help
|
||||
|
||||
# Prompt with optional arguments
|
||||
- name: person_info
|
||||
description: Get information about a person
|
||||
arguments:
|
||||
- name: personId
|
||||
description: The person ID
|
||||
required: true
|
||||
- name: includeAddress
|
||||
description: Include address in the response
|
||||
required: false
|
||||
- name: format
|
||||
description: Output format
|
||||
required: false
|
||||
|
||||
# Prompt with all required arguments
|
||||
- name: compare_people
|
||||
description: Compare two people
|
||||
arguments:
|
||||
- name: person1Id
|
||||
description: First person ID
|
||||
required: true
|
||||
- name: person2Id
|
||||
description: Second person ID
|
||||
required: true
|
||||
@@ -1,31 +0,0 @@
|
||||
// Code generated by mcpgen. DO NOT EDIT.
|
||||
|
||||
package test
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/myapp/models"
|
||||
mcputil "go.probo.inc/mcpgen/mcp"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Tool input schemas
|
||||
var (
|
||||
CreateEventToolInputSchema = mcputil.MustUnmarshalSchema(`{"type":"object","required":["id","name","createdAt"],"properties":{"createdAt":{"$ref":"#/components/schemas/Timestamp"},"id":{"$ref":"#/components/schemas/UUID"},"name":{"type":"string"},"owner":{"$ref":"#/components/schemas/User"}}}`)
|
||||
)
|
||||
|
||||
// Event represents the schema
|
||||
type Event struct {
|
||||
Name string `json:"name"`
|
||||
Owner models.User `json:"owner,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
}
|
||||
|
||||
// CreateEventInput represents the schema
|
||||
type CreateEventInput struct {
|
||||
Name string `json:"name"`
|
||||
Owner models.User `json:"owner,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
info:
|
||||
title: config-based-test
|
||||
version: 1.0.0
|
||||
description: Test config-based custom type mapping
|
||||
|
||||
components:
|
||||
schemas:
|
||||
# These will be mapped via config, not go.probo.inc/mcpgen/type
|
||||
Timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
UUID:
|
||||
type: string
|
||||
format: uuid
|
||||
|
||||
User:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
required: [id, name]
|
||||
|
||||
Event:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
$ref: "#/components/schemas/UUID"
|
||||
name:
|
||||
type: string
|
||||
owner:
|
||||
$ref: "#/components/schemas/User"
|
||||
createdAt:
|
||||
$ref: "#/components/schemas/Timestamp"
|
||||
required: [id, name, createdAt]
|
||||
|
||||
tools:
|
||||
- name: create_event
|
||||
inputSchema:
|
||||
$ref: "#/components/schemas/Event"
|
||||
@@ -1,185 +0,0 @@
|
||||
info:
|
||||
title: custom-types-test
|
||||
version: 1.0.0
|
||||
description: Test all custom type mapping scenarios
|
||||
|
||||
components:
|
||||
schemas:
|
||||
# Standard library types with go.probo.inc/mcpgen/type
|
||||
Timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
description: A timestamp
|
||||
go.probo.inc/mcpgen/type: time.Time
|
||||
|
||||
Duration:
|
||||
type: string
|
||||
description: A duration
|
||||
go.probo.inc/mcpgen/type: time.Duration
|
||||
|
||||
# External package types
|
||||
UUID:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID
|
||||
go.probo.inc/mcpgen/type: github.com/google/uuid.UUID
|
||||
|
||||
Decimal:
|
||||
type: string
|
||||
description: A decimal number
|
||||
go.probo.inc/mcpgen/type: github.com/shopspring/decimal.Decimal
|
||||
|
||||
# JSON raw message
|
||||
Metadata:
|
||||
type: object
|
||||
description: Raw JSON metadata
|
||||
go.probo.inc/mcpgen/type: json.RawMessage
|
||||
|
||||
# Regular enum (should be generated)
|
||||
Status:
|
||||
type: string
|
||||
enum: [pending, in_progress, completed, cancelled]
|
||||
description: Task status
|
||||
|
||||
# Regular object (should be generated)
|
||||
Task:
|
||||
type: object
|
||||
description: A task
|
||||
properties:
|
||||
id:
|
||||
$ref: "#/components/schemas/UUID"
|
||||
title:
|
||||
type: string
|
||||
description: Task title
|
||||
status:
|
||||
$ref: "#/components/schemas/Status"
|
||||
createdAt:
|
||||
$ref: "#/components/schemas/Timestamp"
|
||||
updatedAt:
|
||||
anyOf:
|
||||
- $ref: "#/components/schemas/Timestamp"
|
||||
- type: "null"
|
||||
duration:
|
||||
$ref: "#/components/schemas/Duration"
|
||||
metadata:
|
||||
$ref: "#/components/schemas/Metadata"
|
||||
tags:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Task tags
|
||||
priority:
|
||||
type: integer
|
||||
description: Priority level
|
||||
required: [id, title, status, createdAt]
|
||||
|
||||
# Object with all nullable custom types
|
||||
OptionalFields:
|
||||
type: object
|
||||
properties:
|
||||
optionalTimestamp:
|
||||
anyOf:
|
||||
- $ref: "#/components/schemas/Timestamp"
|
||||
- type: "null"
|
||||
optionalUUID:
|
||||
anyOf:
|
||||
- $ref: "#/components/schemas/UUID"
|
||||
- type: "null"
|
||||
optionalDecimal:
|
||||
anyOf:
|
||||
- $ref: "#/components/schemas/Decimal"
|
||||
- type: "null"
|
||||
|
||||
# Nested objects
|
||||
Project:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
$ref: "#/components/schemas/UUID"
|
||||
name:
|
||||
type: string
|
||||
tasks:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Task"
|
||||
createdAt:
|
||||
$ref: "#/components/schemas/Timestamp"
|
||||
required: [id, name, createdAt]
|
||||
|
||||
# Update input with omittable fields
|
||||
UpdateTaskInput:
|
||||
type: object
|
||||
description: Input for partial task update
|
||||
properties:
|
||||
id:
|
||||
$ref: "#/components/schemas/UUID"
|
||||
description: Task ID to update
|
||||
title:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
description: New title (omit to keep unchanged, null to clear)
|
||||
go.probo.inc/mcpgen/omittable: true
|
||||
status:
|
||||
anyOf:
|
||||
- $ref: "#/components/schemas/Status"
|
||||
- type: "null"
|
||||
description: New status (omit to keep unchanged)
|
||||
go.probo.inc/mcpgen/omittable: true
|
||||
priority:
|
||||
anyOf:
|
||||
- type: integer
|
||||
- type: "null"
|
||||
description: New priority (omit to keep unchanged, null to clear)
|
||||
go.probo.inc/mcpgen/omittable: true
|
||||
tags:
|
||||
anyOf:
|
||||
- type: array
|
||||
items:
|
||||
type: string
|
||||
- type: "null"
|
||||
description: New tags (omit to keep unchanged, null to clear)
|
||||
go.probo.inc/mcpgen/omittable: true
|
||||
required: [id]
|
||||
|
||||
tools:
|
||||
- name: create_task
|
||||
description: Create a new task
|
||||
inputSchema:
|
||||
$ref: "#/components/schemas/Task"
|
||||
|
||||
- name: update_task
|
||||
description: Update task fields (partial update with omittable fields)
|
||||
inputSchema:
|
||||
$ref: "#/components/schemas/UpdateTaskInput"
|
||||
|
||||
- name: create_project
|
||||
description: Create a new project
|
||||
inputSchema:
|
||||
$ref: "#/components/schemas/Project"
|
||||
|
||||
resources:
|
||||
- uriTemplate: "task://{id}"
|
||||
name: Task Resource
|
||||
description: Get a task by ID
|
||||
mimeType: application/json
|
||||
schema:
|
||||
$ref: "#/components/schemas/Task"
|
||||
|
||||
- uriTemplate: "project://{id}"
|
||||
name: Project Resource
|
||||
description: Get a project by ID
|
||||
mimeType: application/json
|
||||
schema:
|
||||
$ref: "#/components/schemas/Project"
|
||||
|
||||
prompts:
|
||||
- name: task_summary
|
||||
description: Generate a task summary
|
||||
arguments:
|
||||
- name: taskId
|
||||
description: The task ID
|
||||
required: true
|
||||
- name: includeMetadata
|
||||
description: Include metadata in summary
|
||||
required: false
|
||||
652
third_party/mcpgen/internal/codegen/types.go
vendored
652
third_party/mcpgen/internal/codegen/types.go
vendored
@@ -1,652 +0,0 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/format"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"go.probo.inc/mcpgen/internal/schema"
|
||||
)
|
||||
|
||||
type CustomTypeMapping struct {
|
||||
GoType string
|
||||
ImportPath string
|
||||
IsPointer bool
|
||||
}
|
||||
|
||||
type TypeGenerator struct {
|
||||
schemas map[string]*schema.Schema
|
||||
types map[string]string
|
||||
enums map[string]string
|
||||
imports map[string]bool
|
||||
schemaVars map[string]string
|
||||
customMappings map[string]*CustomTypeMapping
|
||||
}
|
||||
|
||||
func NewTypeGenerator() *TypeGenerator {
|
||||
return &TypeGenerator{
|
||||
schemas: make(map[string]*schema.Schema),
|
||||
types: make(map[string]string),
|
||||
enums: make(map[string]string),
|
||||
imports: make(map[string]bool),
|
||||
schemaVars: make(map[string]string),
|
||||
customMappings: make(map[string]*CustomTypeMapping),
|
||||
}
|
||||
}
|
||||
|
||||
func (g *TypeGenerator) AddCustomMapping(schemaName string, mapping *CustomTypeMapping) {
|
||||
g.customMappings[schemaName] = mapping
|
||||
}
|
||||
|
||||
func (g *TypeGenerator) AddSchema(name string, s *schema.Schema) {
|
||||
g.schemas[name] = s
|
||||
}
|
||||
|
||||
func (g *TypeGenerator) AddSchemaVar(name string, schemaJSON string) {
|
||||
g.schemaVars[name] = schemaJSON
|
||||
g.imports["go.probo.inc/mcpgen/mcp"] = true
|
||||
}
|
||||
|
||||
func (g *TypeGenerator) Generate(packageName string) ([]byte, error) {
|
||||
var buf strings.Builder
|
||||
|
||||
buf.WriteString("// Code generated by mcpgen. DO NOT EDIT.\n\n")
|
||||
buf.WriteString(fmt.Sprintf("package %s\n\n", packageName))
|
||||
|
||||
// Sort schema names for deterministic output
|
||||
schemaNames := make([]string, 0, len(g.schemas))
|
||||
for name := range g.schemas {
|
||||
schemaNames = append(schemaNames, name)
|
||||
}
|
||||
sort.Strings(schemaNames)
|
||||
|
||||
for _, name := range schemaNames {
|
||||
s := g.schemas[name]
|
||||
typeName := toGoTypeName(name)
|
||||
|
||||
if _, hasCustomMapping := g.customMappings[name]; hasCustomMapping {
|
||||
continue
|
||||
}
|
||||
|
||||
typeCode, err := g.generateType(typeName, s, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate type for %s: %w", name, err)
|
||||
}
|
||||
|
||||
if typeCode != "" && g.types[typeName] == "" {
|
||||
g.types[typeName] = typeCode
|
||||
}
|
||||
}
|
||||
|
||||
if len(g.imports) > 0 {
|
||||
buf.WriteString("import (\n")
|
||||
// Sort imports for deterministic output
|
||||
imports := make([]string, 0, len(g.imports))
|
||||
for imp := range g.imports {
|
||||
imports = append(imports, imp)
|
||||
}
|
||||
sort.Strings(imports)
|
||||
for _, imp := range imports {
|
||||
buf.WriteString(fmt.Sprintf("\t\"%s\"\n", imp))
|
||||
}
|
||||
buf.WriteString(")\n\n")
|
||||
}
|
||||
|
||||
if len(g.schemaVars) > 0 {
|
||||
buf.WriteString("// Tool input schemas\n")
|
||||
buf.WriteString("var (\n")
|
||||
// Sort schema var names for deterministic output
|
||||
varNames := make([]string, 0, len(g.schemaVars))
|
||||
for varName := range g.schemaVars {
|
||||
varNames = append(varNames, varName)
|
||||
}
|
||||
sort.Strings(varNames)
|
||||
for _, varName := range varNames {
|
||||
schemaJSON := g.schemaVars[varName]
|
||||
buf.WriteString(fmt.Sprintf("\t%s = mcp.MustUnmarshalSchema(`%s`)\n", varName, schemaJSON))
|
||||
}
|
||||
buf.WriteString(")\n\n")
|
||||
}
|
||||
|
||||
// Sort enum names for deterministic output
|
||||
enumNames := make([]string, 0, len(g.enums))
|
||||
for enumName := range g.enums {
|
||||
enumNames = append(enumNames, enumName)
|
||||
}
|
||||
sort.Strings(enumNames)
|
||||
for _, enumName := range enumNames {
|
||||
enumCode := g.enums[enumName]
|
||||
buf.WriteString(enumCode)
|
||||
buf.WriteString("\n\n")
|
||||
}
|
||||
|
||||
written := make(map[string]bool)
|
||||
|
||||
// Sort schema names for deterministic output (second pass)
|
||||
for _, name := range schemaNames {
|
||||
typeName := toGoTypeName(name)
|
||||
if typeCode := g.types[typeName]; typeCode != "" {
|
||||
buf.WriteString(typeCode)
|
||||
buf.WriteString("\n\n")
|
||||
written[typeName] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Sort type names for deterministic output
|
||||
typeNames := make([]string, 0, len(g.types))
|
||||
for typeName := range g.types {
|
||||
typeNames = append(typeNames, typeName)
|
||||
}
|
||||
sort.Strings(typeNames)
|
||||
for _, typeName := range typeNames {
|
||||
typeCode := g.types[typeName]
|
||||
if !written[typeName] && typeCode != "" {
|
||||
buf.WriteString(typeCode)
|
||||
buf.WriteString("\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
formatted, err := format.Source([]byte(buf.String()))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to format generated code: %w\n%s", err, buf.String())
|
||||
}
|
||||
|
||||
return formatted, nil
|
||||
}
|
||||
|
||||
func (g *TypeGenerator) generateType(name string, s *schema.Schema, depth int) (string, error) {
|
||||
schemaType := schema.GetType(s)
|
||||
|
||||
if schemaType == "" && s.Properties != nil && len(s.Properties) > 0 {
|
||||
return g.generateStruct(name, s, depth)
|
||||
}
|
||||
|
||||
if schemaType == "" && s.Properties == nil {
|
||||
return "", fmt.Errorf("unsupported schema type: %q (no type and no properties for %s)", schemaType, name)
|
||||
}
|
||||
|
||||
if len(s.Enum) > 0 {
|
||||
return g.generateEnum(name, s)
|
||||
}
|
||||
|
||||
switch schemaType {
|
||||
case "object":
|
||||
return g.generateStruct(name, s, depth)
|
||||
case "array":
|
||||
return g.generateArrayType(name, s, depth)
|
||||
case "string":
|
||||
if depth == 0 {
|
||||
return g.generatePrimitiveTypeAlias(name, s, "string")
|
||||
}
|
||||
return "", nil
|
||||
case "number":
|
||||
if depth == 0 {
|
||||
return g.generatePrimitiveTypeAlias(name, s, "float64")
|
||||
}
|
||||
return "", nil
|
||||
case "integer":
|
||||
if depth == 0 {
|
||||
return g.generatePrimitiveTypeAlias(name, s, "int")
|
||||
}
|
||||
return "", nil
|
||||
case "boolean":
|
||||
if depth == 0 {
|
||||
return g.generatePrimitiveTypeAlias(name, s, "bool")
|
||||
}
|
||||
return "", nil
|
||||
default:
|
||||
if len(s.Properties) > 0 {
|
||||
return g.generateStruct(name, s, depth)
|
||||
}
|
||||
return "", fmt.Errorf("unsupported schema type: %s", schemaType)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *TypeGenerator) generateStruct(name string, s *schema.Schema, depth int) (string, error) {
|
||||
var buf strings.Builder
|
||||
|
||||
if s.Description != "" {
|
||||
buf.WriteString(formatComment(s.Description, ""))
|
||||
} else if s.Title != "" {
|
||||
buf.WriteString(formatComment(s.Title, ""))
|
||||
} else {
|
||||
buf.WriteString(fmt.Sprintf("// %s represents the schema\n", name))
|
||||
}
|
||||
|
||||
buf.WriteString(fmt.Sprintf("type %s struct {\n", name))
|
||||
|
||||
// Sort property names for deterministic output
|
||||
propNames := make([]string, 0, len(s.Properties))
|
||||
for propName := range s.Properties {
|
||||
propNames = append(propNames, propName)
|
||||
}
|
||||
sort.Strings(propNames)
|
||||
|
||||
for _, propName := range propNames {
|
||||
propSchema := s.Properties[propName]
|
||||
fieldName := toGoFieldName(propName)
|
||||
hint := name + fieldName
|
||||
|
||||
isRequired := schema.IsRequired(s, propName)
|
||||
isOmittable := schema.IsOmittable(propSchema)
|
||||
|
||||
// Validate that omittable is only used on nullable fields
|
||||
if isOmittable {
|
||||
isNullable, _ := isNullableType(propSchema)
|
||||
if !isNullable {
|
||||
return "", fmt.Errorf("field %s.%s has omittable annotation but is not nullable (omittable only works with nullable fields)", name, propName)
|
||||
}
|
||||
}
|
||||
|
||||
fieldType, err := g.goType(propSchema, hint)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate field %s: %w", propName, err)
|
||||
}
|
||||
|
||||
if isOmittable {
|
||||
fieldType = fmt.Sprintf("mcp.Omittable[%s]", fieldType)
|
||||
g.imports["go.probo.inc/mcpgen/mcp"] = true
|
||||
} else if !isRequired && !isPointerType(fieldType) {
|
||||
fieldType = "*" + fieldType
|
||||
}
|
||||
|
||||
if propSchema.Description != "" {
|
||||
buf.WriteString(formatComment(propSchema.Description, "\t"))
|
||||
}
|
||||
|
||||
buf.WriteString(fmt.Sprintf("\t%s %s", fieldName, fieldType))
|
||||
|
||||
jsonTag := propName
|
||||
if !isRequired {
|
||||
jsonTag += ",omitempty"
|
||||
}
|
||||
buf.WriteString(fmt.Sprintf(" `json:\"%s\"`", jsonTag))
|
||||
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
|
||||
buf.WriteString("}")
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// isPointerType checks if the given type string is already a pointer or slice type
|
||||
func isPointerType(t string) bool {
|
||||
return len(t) > 0 && (t[0] == '*' || t[0] == '[')
|
||||
}
|
||||
|
||||
func isNullableType(s *schema.Schema) (bool, *schema.Schema) {
|
||||
if len(s.AnyOf) == 2 {
|
||||
var nullIndex = -1
|
||||
var typeIndex = -1
|
||||
|
||||
for i, subSchema := range s.AnyOf {
|
||||
subType := schema.GetType(subSchema)
|
||||
if subType == "null" {
|
||||
nullIndex = i
|
||||
} else if subType != "" || subSchema.Properties != nil || subSchema.Ref != "" {
|
||||
typeIndex = i
|
||||
}
|
||||
}
|
||||
|
||||
if nullIndex >= 0 && typeIndex >= 0 {
|
||||
return true, s.AnyOf[typeIndex]
|
||||
}
|
||||
}
|
||||
|
||||
if len(s.Types) > 0 {
|
||||
hasNull := false
|
||||
var otherType string
|
||||
for _, t := range s.Types {
|
||||
if t == "null" {
|
||||
hasNull = true
|
||||
} else if otherType == "" {
|
||||
otherType = t
|
||||
}
|
||||
}
|
||||
|
||||
if hasNull && otherType != "" && len(s.Types) == 2 {
|
||||
syntheticSchema := &schema.Schema{
|
||||
Type: otherType,
|
||||
Format: s.Format,
|
||||
}
|
||||
return true, syntheticSchema
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (g *TypeGenerator) generateArrayType(name string, s *schema.Schema, depth int) (string, error) {
|
||||
if s.Items == nil {
|
||||
if depth == 0 {
|
||||
return g.generatePrimitiveTypeAlias(name, s, "[]any")
|
||||
}
|
||||
return "[]any", nil
|
||||
}
|
||||
|
||||
itemType, err := g.goType(s.Items, name+"Item")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
arrayType := fmt.Sprintf("[]%s", itemType)
|
||||
|
||||
if depth == 0 {
|
||||
return g.generatePrimitiveTypeAlias(name, s, arrayType)
|
||||
}
|
||||
|
||||
return arrayType, nil
|
||||
}
|
||||
|
||||
func (g *TypeGenerator) goType(s *schema.Schema, hint string) (string, error) {
|
||||
if s.Ref != "" {
|
||||
const prefix = "#/components/schemas/"
|
||||
if len(s.Ref) > len(prefix) && s.Ref[:len(prefix)] == prefix {
|
||||
schemaName := s.Ref[len(prefix):]
|
||||
|
||||
if customMapping, ok := g.customMappings[schemaName]; ok {
|
||||
if customMapping.ImportPath != "" {
|
||||
g.imports[customMapping.ImportPath] = true
|
||||
}
|
||||
if customMapping.IsPointer {
|
||||
return "*" + customMapping.GoType, nil
|
||||
}
|
||||
return customMapping.GoType, nil
|
||||
}
|
||||
|
||||
return "*" + toGoTypeName(schemaName), nil
|
||||
}
|
||||
}
|
||||
|
||||
if nullable, baseType := isNullableType(s); nullable {
|
||||
goType, err := g.goType(baseType, hint)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(goType) > 0 && goType[0] == '*' {
|
||||
return goType, nil
|
||||
}
|
||||
return "*" + goType, nil
|
||||
}
|
||||
|
||||
schemaType := schema.GetType(s)
|
||||
|
||||
switch schemaType {
|
||||
case "string":
|
||||
if len(s.Enum) > 0 {
|
||||
enumTypeName := toGoTypeName(hint)
|
||||
if g.enums[enumTypeName] == "" {
|
||||
enumCode, err := g.generateEnum(enumTypeName, s)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
g.enums[enumTypeName] = enumCode
|
||||
}
|
||||
return enumTypeName, nil
|
||||
}
|
||||
return g.goStringType(s), nil
|
||||
case "number":
|
||||
return "float64", nil
|
||||
case "integer":
|
||||
return "int", nil
|
||||
case "boolean":
|
||||
return "bool", nil
|
||||
case "array":
|
||||
if s.Items == nil {
|
||||
return "[]any", nil
|
||||
}
|
||||
itemType, err := g.goType(s.Items, hint+"Item")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("[]%s", itemType), nil
|
||||
case "object":
|
||||
if s.Title != "" {
|
||||
typeName := toGoTypeName(s.Title)
|
||||
if g.types[typeName] == "" {
|
||||
typeCode, err := g.generateStruct(typeName, s, 0)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
g.types[typeName] = typeCode
|
||||
}
|
||||
return typeName, nil
|
||||
}
|
||||
if len(s.Properties) > 0 {
|
||||
typeName := hint
|
||||
if g.types[typeName] == "" {
|
||||
typeCode, err := g.generateStruct(typeName, s, 0)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
g.types[typeName] = typeCode
|
||||
}
|
||||
return typeName, nil
|
||||
}
|
||||
return "map[string]any", nil
|
||||
case "null":
|
||||
return "any", nil
|
||||
default:
|
||||
if len(s.Properties) > 0 {
|
||||
typeName := toGoTypeName(hint)
|
||||
if g.types[typeName] == "" {
|
||||
typeCode, err := g.generateStruct(typeName, s, 0)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
g.types[typeName] = typeCode
|
||||
}
|
||||
return typeName, nil
|
||||
}
|
||||
return "any", nil
|
||||
}
|
||||
}
|
||||
|
||||
func (g *TypeGenerator) generatePrimitiveTypeAlias(name string, s *schema.Schema, goType string) (string, error) {
|
||||
var buf strings.Builder
|
||||
|
||||
if s.Description != "" {
|
||||
buf.WriteString(formatComment(s.Description, ""))
|
||||
} else {
|
||||
buf.WriteString(fmt.Sprintf("// %s represents a %s schema\n", name, goType))
|
||||
}
|
||||
|
||||
buf.WriteString(fmt.Sprintf("type %s %s", name, goType))
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func (g *TypeGenerator) generateEnum(enumTypeName string, s *schema.Schema) (string, error) {
|
||||
if len(s.Enum) == 0 {
|
||||
return "", fmt.Errorf("schema has no enum values")
|
||||
}
|
||||
|
||||
var buf strings.Builder
|
||||
|
||||
if s.Description != "" {
|
||||
buf.WriteString(formatComment(s.Description, ""))
|
||||
} else {
|
||||
buf.WriteString(fmt.Sprintf("// %s represents an enumeration\n", enumTypeName))
|
||||
}
|
||||
|
||||
buf.WriteString(fmt.Sprintf("type %s string\n\n", enumTypeName))
|
||||
|
||||
buf.WriteString("const (\n")
|
||||
var enumValues []string
|
||||
for i, enumValue := range s.Enum {
|
||||
strValue := fmt.Sprintf("%v", enumValue)
|
||||
enumValues = append(enumValues, strValue)
|
||||
constName := toEnumConstName(enumTypeName, strValue)
|
||||
|
||||
if i == 0 {
|
||||
buf.WriteString(fmt.Sprintf("\t%s %s = %q\n", constName, enumTypeName, strValue))
|
||||
} else {
|
||||
buf.WriteString(fmt.Sprintf("\t%s %s = %q\n", constName, enumTypeName, strValue))
|
||||
}
|
||||
}
|
||||
buf.WriteString(")\n\n")
|
||||
|
||||
// Generate validation method
|
||||
buf.WriteString(fmt.Sprintf("// IsValid returns true if the %s value is valid\n", enumTypeName))
|
||||
buf.WriteString(fmt.Sprintf("func (e %s) IsValid() bool {\n", enumTypeName))
|
||||
buf.WriteString("\tswitch e {\n")
|
||||
for _, strValue := range enumValues {
|
||||
constName := toEnumConstName(enumTypeName, strValue)
|
||||
buf.WriteString(fmt.Sprintf("\tcase %s:\n\t\treturn true\n", constName))
|
||||
}
|
||||
buf.WriteString("\t}\n")
|
||||
buf.WriteString("\treturn false\n")
|
||||
buf.WriteString("}\n\n")
|
||||
|
||||
// Generate UnmarshalJSON method
|
||||
buf.WriteString("// UnmarshalJSON implements json.Unmarshaler\n")
|
||||
buf.WriteString(fmt.Sprintf("func (e *%s) UnmarshalJSON(data []byte) error {\n", enumTypeName))
|
||||
buf.WriteString("\tvar s string\n")
|
||||
buf.WriteString("\tif err := json.Unmarshal(data, &s); err != nil {\n")
|
||||
buf.WriteString("\t\treturn err\n")
|
||||
buf.WriteString("\t}\n")
|
||||
buf.WriteString(fmt.Sprintf("\t*e = %s(s)\n", enumTypeName))
|
||||
buf.WriteString("\tif !e.IsValid() {\n")
|
||||
buf.WriteString(fmt.Sprintf("\t\treturn fmt.Errorf(\"invalid %s value: %%q\", s)\n", enumTypeName))
|
||||
buf.WriteString("\t}\n")
|
||||
buf.WriteString("\treturn nil\n")
|
||||
buf.WriteString("}\n\n")
|
||||
|
||||
// Generate MarshalJSON method
|
||||
buf.WriteString("// MarshalJSON implements json.Marshaler\n")
|
||||
buf.WriteString(fmt.Sprintf("func (e %s) MarshalJSON() ([]byte, error) {\n", enumTypeName))
|
||||
buf.WriteString("\tif !e.IsValid() {\n")
|
||||
buf.WriteString(fmt.Sprintf("\t\treturn nil, fmt.Errorf(\"invalid %s value: %%q\", string(e))\n", enumTypeName))
|
||||
buf.WriteString("\t}\n")
|
||||
buf.WriteString("\treturn json.Marshal(string(e))\n")
|
||||
buf.WriteString("}")
|
||||
|
||||
g.imports["encoding/json"] = true
|
||||
g.imports["fmt"] = true
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func (g *TypeGenerator) goStringType(s *schema.Schema) string {
|
||||
switch s.Format {
|
||||
case "date-time":
|
||||
g.imports["time"] = true
|
||||
return "time.Time"
|
||||
case "date", "time", "email", "hostname", "ipv4", "ipv6", "uri", "uuid":
|
||||
return "string"
|
||||
default:
|
||||
return "string"
|
||||
}
|
||||
}
|
||||
|
||||
func toGoTypeName(name string) string {
|
||||
name = strings.TrimSuffix(name, ".json")
|
||||
name = strings.TrimSuffix(name, "_input")
|
||||
name = strings.TrimSuffix(name, "_output")
|
||||
name = strings.TrimSuffix(name, "_schema")
|
||||
|
||||
parts := strings.FieldsFunc(name, func(r rune) bool {
|
||||
return r == '_' || r == '-' || r == ' ' || r == '.'
|
||||
})
|
||||
|
||||
for i, part := range parts {
|
||||
if len(part) > 0 {
|
||||
parts[i] = strings.ToUpper(part[:1]) + part[1:]
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(parts, "")
|
||||
}
|
||||
|
||||
var goAcronyms = map[string]bool{
|
||||
"acl": true,
|
||||
"api": true,
|
||||
"ascii": true,
|
||||
"cpu": true,
|
||||
"css": true,
|
||||
"dns": true,
|
||||
"eof": true,
|
||||
"guid": true,
|
||||
"html": true,
|
||||
"http": true,
|
||||
"https": true,
|
||||
"id": true,
|
||||
"ip": true,
|
||||
"json": true,
|
||||
"jwt": true,
|
||||
"lhs": true,
|
||||
"qps": true,
|
||||
"ram": true,
|
||||
"rhs": true,
|
||||
"rpc": true,
|
||||
"sla": true,
|
||||
"smtp": true,
|
||||
"sql": true,
|
||||
"ssh": true,
|
||||
"tcp": true,
|
||||
"tls": true,
|
||||
"ttl": true,
|
||||
"udp": true,
|
||||
"ui": true,
|
||||
"uid": true,
|
||||
"uri": true,
|
||||
"url": true,
|
||||
"utf": true,
|
||||
"uuid": true,
|
||||
"vm": true,
|
||||
"xml": true,
|
||||
}
|
||||
|
||||
var goSpecialCase = map[string]string{
|
||||
"oauth": "OAuth",
|
||||
}
|
||||
|
||||
func toGoFieldName(name string) string {
|
||||
parts := strings.FieldsFunc(name, func(r rune) bool {
|
||||
return r == '_' || r == '-' || r == ' '
|
||||
})
|
||||
|
||||
for i, part := range parts {
|
||||
if len(part) > 0 {
|
||||
lowerPart := strings.ToLower(part)
|
||||
if specialCase, ok := goSpecialCase[lowerPart]; ok {
|
||||
parts[i] = specialCase
|
||||
} else if goAcronyms[lowerPart] {
|
||||
parts[i] = strings.ToUpper(part)
|
||||
} else {
|
||||
parts[i] = strings.ToUpper(part[:1]) + part[1:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(parts, "")
|
||||
}
|
||||
|
||||
func formatComment(text, prefix string) string {
|
||||
lines := strings.Split(strings.TrimSpace(text), "\n")
|
||||
var result strings.Builder
|
||||
|
||||
for _, line := range lines {
|
||||
result.WriteString(fmt.Sprintf("%s// %s\n", prefix, strings.TrimSpace(line)))
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func toEnumConstName(enumTypeName, value string) string {
|
||||
parts := strings.FieldsFunc(value, func(r rune) bool {
|
||||
return r == '_' || r == '-' || r == ' ' || r == '.'
|
||||
})
|
||||
|
||||
for i, part := range parts {
|
||||
if len(part) > 0 {
|
||||
parts[i] = strings.ToUpper(part[:1]) + part[1:]
|
||||
}
|
||||
}
|
||||
|
||||
constName := strings.Join(parts, "")
|
||||
baseName := strings.TrimSuffix(enumTypeName, "Type")
|
||||
|
||||
return baseName + constName
|
||||
}
|
||||
1048
third_party/mcpgen/internal/codegen/types_test.go
vendored
1048
third_party/mcpgen/internal/codegen/types_test.go
vendored
File diff suppressed because it is too large
Load Diff
208
third_party/mcpgen/internal/config/config.go
vendored
208
third_party/mcpgen/internal/config/config.go
vendored
@@ -1,208 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/jsonschema-go/jsonschema"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Spec string `yaml:"spec" json:"spec"`
|
||||
Output string `yaml:"output" json:"output"`
|
||||
Exec ExecConfig `yaml:"exec,omitempty" json:"exec,omitempty"`
|
||||
Resolver ResolverConfig `yaml:"resolver" json:"resolver"`
|
||||
Model ModelConfig `yaml:"model,omitempty" json:"model,omitempty"`
|
||||
Models ModelsConfig `yaml:"models,omitempty" json:"models,omitempty"`
|
||||
}
|
||||
|
||||
type ExecConfig struct {
|
||||
Package string `yaml:"package,omitempty" json:"package,omitempty"`
|
||||
Filename string `yaml:"filename,omitempty" json:"filename,omitempty"`
|
||||
}
|
||||
|
||||
type ResolverConfig struct {
|
||||
Package string `yaml:"package" json:"package"`
|
||||
Filename string `yaml:"filename" json:"filename"`
|
||||
Type string `yaml:"type" json:"type"`
|
||||
Preserve bool `yaml:"preserve" json:"preserve"`
|
||||
}
|
||||
|
||||
type ModelConfig struct {
|
||||
Package string `yaml:"package,omitempty" json:"package,omitempty"`
|
||||
Filename string `yaml:"filename,omitempty" json:"filename,omitempty"`
|
||||
}
|
||||
|
||||
type ModelsConfig struct {
|
||||
// Map schema names to custom Go types
|
||||
// Example: User: github.com/myorg/models.User
|
||||
Models map[string]TypeMapping `yaml:",inline,omitempty" json:",inline,omitempty"`
|
||||
}
|
||||
|
||||
type TypeMapping struct {
|
||||
// Model is the fully qualified Go type to use
|
||||
// Example: github.com/google/uuid.UUID
|
||||
Model string `yaml:"model" json:"model"`
|
||||
}
|
||||
|
||||
type ServerInfo struct {
|
||||
Title string `yaml:"title" json:"title"`
|
||||
Version string `yaml:"version" json:"version"`
|
||||
Description string `yaml:"description,omitempty" json:"description,omitempty"`
|
||||
}
|
||||
|
||||
type Components struct {
|
||||
Schemas map[string]*jsonschema.Schema `yaml:"schemas,omitempty" json:"schemas,omitempty"`
|
||||
}
|
||||
|
||||
type Schema = jsonschema.Schema
|
||||
|
||||
type ToolHints struct {
|
||||
Readonly bool `yaml:"readonly,omitempty" json:"readonly,omitempty"`
|
||||
Destructive bool `yaml:"destructive,omitempty" json:"destructive,omitempty"`
|
||||
Idempotent bool `yaml:"idempotent,omitempty" json:"idempotent,omitempty"`
|
||||
OpenWorld bool `yaml:"openWorld,omitempty" json:"openWorld,omitempty"`
|
||||
}
|
||||
|
||||
type Tool struct {
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Title string `yaml:"title,omitempty" json:"title,omitempty"`
|
||||
Description string `yaml:"description,omitempty" json:"description,omitempty"`
|
||||
InputSchema *Schema `yaml:"inputSchema" json:"inputSchema"`
|
||||
OutputSchema *Schema `yaml:"outputSchema,omitempty" json:"outputSchema,omitempty"`
|
||||
Hints *ToolHints `yaml:"hints,omitempty" json:"hints,omitempty"`
|
||||
Annotations map[string]string `yaml:"annotations,omitempty" json:"annotations,omitempty"`
|
||||
Handler string `yaml:"handler,omitempty" json:"handler,omitempty"`
|
||||
}
|
||||
|
||||
type Resource struct {
|
||||
URI string `yaml:"uri,omitempty" json:"uri,omitempty"`
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Description string `yaml:"description,omitempty" json:"description,omitempty"`
|
||||
MimeType string `yaml:"mimeType,omitempty" json:"mimeType,omitempty"`
|
||||
URITemplate string `yaml:"uriTemplate,omitempty" json:"uriTemplate,omitempty"`
|
||||
Schema *Schema `yaml:"schema,omitempty" json:"schema,omitempty"`
|
||||
Readonly bool `yaml:"readonly,omitempty" json:"readonly,omitempty"`
|
||||
Annotations map[string]string `yaml:"annotations,omitempty" json:"annotations,omitempty"`
|
||||
Handler string `yaml:"handler,omitempty" json:"handler,omitempty"`
|
||||
}
|
||||
|
||||
type Prompt struct {
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Description string `yaml:"description,omitempty" json:"description,omitempty"`
|
||||
Arguments []PromptArgument `yaml:"arguments,omitempty" json:"arguments,omitempty"`
|
||||
Annotations map[string]string `yaml:"annotations,omitempty" json:"annotations,omitempty"`
|
||||
Handler string `yaml:"handler,omitempty" json:"handler,omitempty"`
|
||||
}
|
||||
|
||||
type PromptArgument struct {
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Description string `yaml:"description,omitempty" json:"description,omitempty"`
|
||||
Required bool `yaml:"required,omitempty" json:"required,omitempty"`
|
||||
}
|
||||
|
||||
func Load(path string) (*Config, *MCPSpec, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
|
||||
config := &Config{
|
||||
Spec: "schema.yaml",
|
||||
Output: "generated",
|
||||
Exec: ExecConfig{
|
||||
Package: "server",
|
||||
Filename: "server/server.go",
|
||||
},
|
||||
Resolver: ResolverConfig{
|
||||
Package: "generated",
|
||||
Filename: "resolver.go",
|
||||
Type: "Resolver",
|
||||
Preserve: true,
|
||||
},
|
||||
Model: ModelConfig{
|
||||
Package: "generated",
|
||||
Filename: "models.go",
|
||||
},
|
||||
}
|
||||
|
||||
ext := filepath.Ext(path)
|
||||
switch ext {
|
||||
case ".yaml", ".yml":
|
||||
if err := yaml.Unmarshal(data, config); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to parse YAML config: %w", err)
|
||||
}
|
||||
case ".json":
|
||||
if err := json.Unmarshal(data, config); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to parse JSON config: %w", err)
|
||||
}
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("unsupported config file format: %s (use .yaml, .yml, or .json)", ext)
|
||||
}
|
||||
|
||||
if err := config.Validate(); err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid configuration: %w", err)
|
||||
}
|
||||
|
||||
// Make output path absolute relative to config file directory
|
||||
configDir := filepath.Dir(path)
|
||||
if !filepath.IsAbs(config.Output) {
|
||||
config.Output = filepath.Join(configDir, config.Output)
|
||||
}
|
||||
|
||||
specPath := config.Spec
|
||||
if !filepath.IsAbs(specPath) {
|
||||
configDir := filepath.Dir(path)
|
||||
specPath = filepath.Join(configDir, specPath)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(specPath); os.IsNotExist(err) {
|
||||
basePath := specPath
|
||||
for _, ext := range []string{".yaml", ".yml", ".json"} {
|
||||
tryPath := basePath
|
||||
if filepath.Ext(tryPath) == "" {
|
||||
tryPath = basePath + ext
|
||||
} else {
|
||||
tryPath = basePath[:len(basePath)-len(filepath.Ext(basePath))] + ext
|
||||
}
|
||||
if _, err := os.Stat(tryPath); err == nil {
|
||||
specPath = tryPath
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
spec, err := LoadMCPSpec(specPath)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to load MCP spec from %s: %w", specPath, err)
|
||||
}
|
||||
|
||||
return config, spec, nil
|
||||
}
|
||||
|
||||
func (c *Config) Validate() error {
|
||||
if c.Spec == "" {
|
||||
return fmt.Errorf("spec path is required")
|
||||
}
|
||||
if c.Output == "" {
|
||||
return fmt.Errorf("output is required")
|
||||
}
|
||||
if c.Exec.Package == "" {
|
||||
return fmt.Errorf("exec.package is required")
|
||||
}
|
||||
if c.Resolver.Package == "" {
|
||||
return fmt.Errorf("resolver.package is required")
|
||||
}
|
||||
if c.Model.Package == "" {
|
||||
return fmt.Errorf("model.package is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func IsSchemaRef(s *Schema) bool {
|
||||
return s != nil && s.Ref != ""
|
||||
}
|
||||
114
third_party/mcpgen/internal/config/spec.go
vendored
114
third_party/mcpgen/internal/config/spec.go
vendored
@@ -1,114 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type MCPSpec struct {
|
||||
Info ServerInfo `yaml:"info" json:"info"`
|
||||
Components Components `yaml:"components,omitempty" json:"components,omitempty"`
|
||||
Tools []Tool `yaml:"tools,omitempty" json:"tools,omitempty"`
|
||||
Resources []Resource `yaml:"resources,omitempty" json:"resources,omitempty"`
|
||||
Prompts []Prompt `yaml:"prompts,omitempty" json:"prompts,omitempty"`
|
||||
}
|
||||
|
||||
func LoadMCPSpec(path string) (*MCPSpec, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read MCP spec file: %w", err)
|
||||
}
|
||||
|
||||
spec := &MCPSpec{}
|
||||
|
||||
ext := filepath.Ext(path)
|
||||
switch ext {
|
||||
case ".yaml", ".yml":
|
||||
var intermediate interface{}
|
||||
if err := yaml.Unmarshal(data, &intermediate); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse YAML spec: %w", err)
|
||||
}
|
||||
jsonData, err := json.Marshal(intermediate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert YAML to JSON: %w", err)
|
||||
}
|
||||
if err := json.Unmarshal(jsonData, spec); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal spec: %w", err)
|
||||
}
|
||||
case ".json":
|
||||
if err := json.Unmarshal(data, spec); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse JSON spec: %w", err)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported spec file format: %s (use .yaml, .yml, or .json)", ext)
|
||||
}
|
||||
|
||||
if err := spec.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid MCP specification: %w", err)
|
||||
}
|
||||
|
||||
return spec, nil
|
||||
}
|
||||
|
||||
func (s *MCPSpec) Validate() error {
|
||||
if s.Info.Title == "" {
|
||||
return fmt.Errorf("info.title is required")
|
||||
}
|
||||
if s.Info.Version == "" {
|
||||
return fmt.Errorf("info.version is required")
|
||||
}
|
||||
|
||||
for i, tool := range s.Tools {
|
||||
if tool.Name == "" {
|
||||
return fmt.Errorf("tools[%d].name is required", i)
|
||||
}
|
||||
if tool.InputSchema == nil {
|
||||
return fmt.Errorf("tools[%d].inputSchema is required", i)
|
||||
}
|
||||
}
|
||||
|
||||
for i, resource := range s.Resources {
|
||||
if resource.Name == "" {
|
||||
return fmt.Errorf("resources[%d].name is required", i)
|
||||
}
|
||||
if resource.URI == "" && resource.URITemplate == "" {
|
||||
return fmt.Errorf("resources[%d] must have either uri or uriTemplate", i)
|
||||
}
|
||||
if resource.URI != "" && resource.URITemplate != "" {
|
||||
return fmt.Errorf("resources[%d] cannot have both uri and uriTemplate", i)
|
||||
}
|
||||
}
|
||||
|
||||
for i, prompt := range s.Prompts {
|
||||
if prompt.Name == "" {
|
||||
return fmt.Errorf("prompts[%d].name is required", i)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MCPSpec) ResolveSchemaRef(ref string) (*Schema, error) {
|
||||
if len(ref) > 0 && ref[0] == '#' {
|
||||
if ref == "#/components/schemas" {
|
||||
return nil, fmt.Errorf("incomplete schema reference: %s", ref)
|
||||
}
|
||||
|
||||
const prefix = "#/components/schemas/"
|
||||
if len(ref) > len(prefix) && ref[:len(prefix)] == prefix {
|
||||
schemaName := ref[len(prefix):]
|
||||
if schema, ok := s.Components.Schemas[schemaName]; ok {
|
||||
return schema, nil
|
||||
}
|
||||
return nil, fmt.Errorf("schema not found: %s", schemaName)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unsupported reference format: %s", ref)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
85
third_party/mcpgen/internal/schema/schema.go
vendored
85
third_party/mcpgen/internal/schema/schema.go
vendored
@@ -1,85 +0,0 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/jsonschema-go/jsonschema"
|
||||
)
|
||||
|
||||
type Schema = jsonschema.Schema
|
||||
|
||||
type Loader struct {
|
||||
schemas map[string]*Schema
|
||||
baseDir string
|
||||
}
|
||||
|
||||
func NewLoader(baseDir string) *Loader {
|
||||
return &Loader{
|
||||
schemas: make(map[string]*Schema),
|
||||
baseDir: baseDir,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Loader) Load(path string) (*Schema, error) {
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get absolute path: %w", err)
|
||||
}
|
||||
|
||||
if schema, ok := l.schemas[absPath]; ok {
|
||||
return schema, nil
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(absPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read schema file %s: %w", path, err)
|
||||
}
|
||||
|
||||
var schema Schema
|
||||
if err := json.Unmarshal(data, &schema); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse schema file %s: %w", path, err)
|
||||
}
|
||||
|
||||
l.schemas[absPath] = &schema
|
||||
|
||||
return &schema, nil
|
||||
}
|
||||
|
||||
func GetType(s *Schema) string {
|
||||
if s.Type != "" {
|
||||
return s.Type
|
||||
}
|
||||
if len(s.Types) > 0 {
|
||||
return s.Types[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func IsRequired(s *Schema, propName string) bool {
|
||||
for _, req := range s.Required {
|
||||
if req == propName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsOmittable checks if a schema property has the go.probo.inc/mcpgen/omittable annotation set to true.
|
||||
// This is used to wrap fields in mcp.Omittable[T] to distinguish between
|
||||
// "not set", "set to null", and "set to value".
|
||||
func IsOmittable(s *Schema) bool {
|
||||
if s == nil || s.Extra == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if omittable, ok := s.Extra["go.probo.inc/mcpgen/omittable"]; ok {
|
||||
if omittableBool, ok := omittable.(bool); ok {
|
||||
return omittableBool
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
185
third_party/mcpgen/main.go
vendored
185
third_party/mcpgen/main.go
vendored
@@ -1,185 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.probo.inc/mcpgen/internal/codegen"
|
||||
"go.probo.inc/mcpgen/internal/config"
|
||||
)
|
||||
|
||||
var version = "dev"
|
||||
|
||||
func main() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "mcpgen",
|
||||
Short: "A code generator for Model Context Protocol (MCP) servers",
|
||||
Long: `mcpgen is a gqlgen-like code generator for building MCP servers in Go.
|
||||
It generates type-safe Go code from JSON Schema definitions for tools, resources, and prompts.`,
|
||||
}
|
||||
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print the version number of mcpgen",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("mcpgen %s\n", version)
|
||||
},
|
||||
}
|
||||
|
||||
var generateCmd = &cobra.Command{
|
||||
Use: "generate",
|
||||
Short: "Generate Go code from mcpgen configuration",
|
||||
Long: `Reads mcpgen.yaml (or mcpgen.yml) configuration file and generates:
|
||||
- Type-safe Go structs from JSON Schemas
|
||||
- MCP server boilerplate code
|
||||
- Handler function stubs for tools, resources, and prompts`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
configFile, _ := cmd.Flags().GetString("config")
|
||||
return runGenerate(configFile)
|
||||
},
|
||||
}
|
||||
|
||||
var initCmd = &cobra.Command{
|
||||
Use: "init [name]",
|
||||
Short: "Initialize a new MCP server project",
|
||||
Long: `Creates a new MCP server project with example configuration and file structure.`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
name := "my-mcp-server"
|
||||
if len(args) > 0 {
|
||||
name = args[0]
|
||||
}
|
||||
return runInit(name)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
generateCmd.Flags().StringP("config", "c", "mcpgen.yaml", "Path to config file")
|
||||
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
rootCmd.AddCommand(generateCmd)
|
||||
rootCmd.AddCommand(initCmd)
|
||||
}
|
||||
|
||||
func runGenerate(configFile string) error {
|
||||
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
||||
if configFile == "mcpgen.yaml" {
|
||||
if _, err := os.Stat("mcpgen.yml"); err == nil {
|
||||
configFile = "mcpgen.yml"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("Loading configuration from %s...\n", configFile)
|
||||
|
||||
cfg, spec, err := config.Load(configFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load configuration: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Generating code for %s v%s...\n", spec.Info.Title, spec.Info.Version)
|
||||
|
||||
gen := codegen.New(cfg, spec)
|
||||
|
||||
if err := gen.Generate(); err != nil {
|
||||
return fmt.Errorf("code generation failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("✓ Code generation completed successfully!")
|
||||
return nil
|
||||
}
|
||||
|
||||
func runInit(name string) error {
|
||||
fmt.Printf("Initializing new MCP server project: %s\n", name)
|
||||
|
||||
if err := os.MkdirAll(name, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create project directory: %w", err)
|
||||
}
|
||||
|
||||
configContent := `# mcpgen configuration
|
||||
# Path to MCP API specification
|
||||
spec: schema.yaml
|
||||
|
||||
# Output directory for generated code
|
||||
output: generated
|
||||
|
||||
# Resolver configuration
|
||||
resolver:
|
||||
package: generated
|
||||
filename: resolver.go
|
||||
type: Resolver
|
||||
preserve: true
|
||||
|
||||
# Model configuration
|
||||
model:
|
||||
package: generated
|
||||
filename: models.go
|
||||
`
|
||||
|
||||
configPath := filepath.Join(name, "mcpgen.yaml")
|
||||
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write config file: %w", err)
|
||||
}
|
||||
|
||||
schemaContent := fmt.Sprintf(`# MCP API Specification
|
||||
# This file contains the pure MCP API definition
|
||||
|
||||
info:
|
||||
title: %s
|
||||
version: 1.0.0
|
||||
description: An example MCP server
|
||||
|
||||
# Reusable schema components
|
||||
components:
|
||||
schemas:
|
||||
ExampleInput:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
description: The message to process
|
||||
required: [message]
|
||||
|
||||
# MCP Tools
|
||||
tools:
|
||||
- name: example_tool
|
||||
title: Example Tool
|
||||
description: An example tool that processes messages
|
||||
hints:
|
||||
readonly: false
|
||||
destructive: false
|
||||
idempotent: true
|
||||
inputSchema:
|
||||
$ref: "#/components/schemas/ExampleInput"
|
||||
|
||||
# MCP Resources
|
||||
resources: []
|
||||
|
||||
# MCP Prompts
|
||||
prompts: []
|
||||
`, name)
|
||||
|
||||
schemaPath := filepath.Join(name, "schema.yaml")
|
||||
if err := os.WriteFile(schemaPath, []byte(schemaContent), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write schema file: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("\n✓ Project initialized successfully!\n\n")
|
||||
fmt.Printf("Files created:\n")
|
||||
fmt.Printf(" - mcpgen.yaml (code generation configuration)\n")
|
||||
fmt.Printf(" - schema.yaml (MCP API specification)\n\n")
|
||||
fmt.Printf("Next steps:\n")
|
||||
fmt.Printf(" cd %s\n", name)
|
||||
fmt.Printf(" # Edit schema.yaml to define your tools, resources, and prompts\n")
|
||||
fmt.Printf(" mcpgen generate\n")
|
||||
|
||||
return nil
|
||||
}
|
||||
117
third_party/mcpgen/mcp/omittable.go
vendored
117
third_party/mcpgen/mcp/omittable.go
vendored
@@ -1,117 +0,0 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Omittable represents a value that can be in one of three states:
|
||||
// 1. Not set (field was not provided in JSON)
|
||||
// 2. Explicitly set to null
|
||||
// 3. Set to a value
|
||||
//
|
||||
// This is useful for distinguishing between "don't update this field" (not set)
|
||||
// and "set this field to null" (explicitly null) in update operations.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// type UpdateUserInput struct {
|
||||
// Name Omittable[string] `json:"name,omitempty"`
|
||||
// Email Omittable[string] `json:"email,omitempty"`
|
||||
// }
|
||||
//
|
||||
// func (r *Resolver) UpdateUser(input UpdateUserInput) {
|
||||
// if input.Name.IsSet() {
|
||||
// if input.Name.IsNull() {
|
||||
// // Set name to null
|
||||
// } else {
|
||||
// // Update name to input.Name.Value()
|
||||
// }
|
||||
// }
|
||||
// // If !IsSet(), don't touch the name field
|
||||
// }
|
||||
type Omittable[T any] struct {
|
||||
value *T
|
||||
isSet bool
|
||||
}
|
||||
|
||||
func NewOmittable[T any](value T) Omittable[T] {
|
||||
return Omittable[T]{
|
||||
value: &value,
|
||||
isSet: true,
|
||||
}
|
||||
}
|
||||
|
||||
func NewOmittableNull[T any]() Omittable[T] {
|
||||
return Omittable[T]{
|
||||
value: nil,
|
||||
isSet: true,
|
||||
}
|
||||
}
|
||||
|
||||
// IsSet returns true if the field was provided in the input (either null or a value).
|
||||
func (o Omittable[T]) IsSet() bool {
|
||||
return o.isSet
|
||||
}
|
||||
|
||||
// IsNull returns true if the field was explicitly set to null.
|
||||
// Returns false if the field was not set or has a value.
|
||||
func (o Omittable[T]) IsNull() bool {
|
||||
return o.isSet && o.value == nil
|
||||
}
|
||||
|
||||
// Value returns the value and a boolean indicating if it has a non-null value.
|
||||
// If the field is not set or is null, returns the zero value and false.
|
||||
func (o Omittable[T]) Value() (T, bool) {
|
||||
if o.value != nil {
|
||||
return *o.value, true
|
||||
}
|
||||
var zero T
|
||||
return zero, false
|
||||
}
|
||||
|
||||
func (o Omittable[T]) ValueOrZero() T {
|
||||
if o.value != nil {
|
||||
return *o.value
|
||||
}
|
||||
var zero T
|
||||
return zero
|
||||
}
|
||||
|
||||
func (o Omittable[T]) Ptr() *T {
|
||||
return o.value
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
func (o *Omittable[T]) UnmarshalJSON(data []byte) error {
|
||||
o.isSet = true
|
||||
|
||||
// Handle explicit null
|
||||
if string(data) == "null" {
|
||||
o.value = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unmarshal the actual value
|
||||
var value T
|
||||
if err := json.Unmarshal(data, &value); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal omittable value: %w", err)
|
||||
}
|
||||
|
||||
o.value = &value
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler.
|
||||
func (o Omittable[T]) MarshalJSON() ([]byte, error) {
|
||||
// Note: When marshaling structs with Omittable fields, use *Omittable[T]
|
||||
// if you need omitempty to work correctly. With value types, omitempty
|
||||
// doesn't work well with custom MarshalJSON.
|
||||
// For MCP use cases (unmarshaling input), this is not typically an issue.
|
||||
|
||||
if !o.isSet || o.value == nil {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
|
||||
return json.Marshal(*o.value)
|
||||
}
|
||||
201
third_party/mcpgen/mcp/omittable_test.go
vendored
201
third_party/mcpgen/mcp/omittable_test.go
vendored
@@ -1,201 +0,0 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestOmittable_NotSet(t *testing.T) {
|
||||
var o Omittable[string]
|
||||
|
||||
assert.False(t, o.IsSet())
|
||||
assert.False(t, o.IsNull())
|
||||
|
||||
value, ok := o.Value()
|
||||
assert.False(t, ok)
|
||||
assert.Equal(t, "", value)
|
||||
}
|
||||
|
||||
func TestOmittable_SetToValue(t *testing.T) {
|
||||
o := NewOmittable("hello")
|
||||
|
||||
assert.True(t, o.IsSet())
|
||||
assert.False(t, o.IsNull())
|
||||
|
||||
value, ok := o.Value()
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "hello", value)
|
||||
assert.Equal(t, "hello", o.ValueOrZero())
|
||||
|
||||
ptr := o.Ptr()
|
||||
require.NotNil(t, ptr)
|
||||
assert.Equal(t, "hello", *ptr)
|
||||
}
|
||||
|
||||
func TestOmittable_SetToNull(t *testing.T) {
|
||||
o := NewOmittableNull[string]()
|
||||
|
||||
assert.True(t, o.IsSet())
|
||||
assert.True(t, o.IsNull())
|
||||
|
||||
value, ok := o.Value()
|
||||
assert.False(t, ok)
|
||||
assert.Equal(t, "", value)
|
||||
assert.Equal(t, "", o.ValueOrZero())
|
||||
assert.Nil(t, o.Ptr())
|
||||
}
|
||||
|
||||
func TestOmittable_UnmarshalJSON_NotProvided(t *testing.T) {
|
||||
type Input struct {
|
||||
Name Omittable[string] `json:"name,omitempty"`
|
||||
Email Omittable[string] `json:"email,omitempty"`
|
||||
}
|
||||
|
||||
jsonData := `{"name": "John"}`
|
||||
var input Input
|
||||
require.NoError(t, json.Unmarshal([]byte(jsonData), &input))
|
||||
|
||||
assert.True(t, input.Name.IsSet())
|
||||
name, ok := input.Name.Value()
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "John", name)
|
||||
|
||||
assert.False(t, input.Email.IsSet())
|
||||
}
|
||||
|
||||
func TestOmittable_UnmarshalJSON_ExplicitNull(t *testing.T) {
|
||||
type Input struct {
|
||||
Name Omittable[string] `json:"name,omitempty"`
|
||||
Email Omittable[string] `json:"email,omitempty"`
|
||||
}
|
||||
|
||||
jsonData := `{"name": null, "email": "test@example.com"}`
|
||||
var input Input
|
||||
require.NoError(t, json.Unmarshal([]byte(jsonData), &input))
|
||||
|
||||
assert.True(t, input.Name.IsSet())
|
||||
assert.True(t, input.Name.IsNull())
|
||||
|
||||
assert.True(t, input.Email.IsSet())
|
||||
email, ok := input.Email.Value()
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "test@example.com", email)
|
||||
}
|
||||
|
||||
func TestOmittable_UnmarshalJSON_WithValue(t *testing.T) {
|
||||
type Input struct {
|
||||
Count Omittable[int] `json:"count,omitempty"`
|
||||
}
|
||||
|
||||
jsonData := `{"count": 42}`
|
||||
var input Input
|
||||
require.NoError(t, json.Unmarshal([]byte(jsonData), &input))
|
||||
|
||||
assert.True(t, input.Count.IsSet())
|
||||
assert.False(t, input.Count.IsNull())
|
||||
count, ok := input.Count.Value()
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, 42, count)
|
||||
}
|
||||
|
||||
func TestOmittable_MarshalJSON_NotSet(t *testing.T) {
|
||||
type Output struct {
|
||||
Name Omittable[string] `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
output := Output{}
|
||||
data, err := json.Marshal(output)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.JSONEq(t, `{"name":null}`, string(data))
|
||||
}
|
||||
|
||||
func TestOmittable_MarshalJSON_Null(t *testing.T) {
|
||||
type Output struct {
|
||||
Name Omittable[string] `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
output := Output{
|
||||
Name: NewOmittableNull[string](),
|
||||
}
|
||||
data, err := json.Marshal(output)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.JSONEq(t, `{"name":null}`, string(data))
|
||||
}
|
||||
|
||||
func TestOmittable_MarshalJSON_WithValue(t *testing.T) {
|
||||
type Output struct {
|
||||
Name Omittable[string] `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
output := Output{
|
||||
Name: NewOmittable("Alice"),
|
||||
}
|
||||
data, err := json.Marshal(output)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.JSONEq(t, `{"name":"Alice"}`, string(data))
|
||||
}
|
||||
|
||||
func TestOmittable_ComplexTypes(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string `json:"name"`
|
||||
Age int `json:"age"`
|
||||
}
|
||||
|
||||
type Input struct {
|
||||
Person Omittable[Person] `json:"person,omitempty"`
|
||||
}
|
||||
|
||||
t.Run("with value", func(t *testing.T) {
|
||||
jsonData := `{"person": {"name": "John", "age": 30}}`
|
||||
var input Input
|
||||
require.NoError(t, json.Unmarshal([]byte(jsonData), &input))
|
||||
|
||||
assert.True(t, input.Person.IsSet())
|
||||
person, ok := input.Person.Value()
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "John", person.Name)
|
||||
assert.Equal(t, 30, person.Age)
|
||||
})
|
||||
|
||||
t.Run("with null", func(t *testing.T) {
|
||||
jsonData := `{"person": null}`
|
||||
var input Input
|
||||
require.NoError(t, json.Unmarshal([]byte(jsonData), &input))
|
||||
|
||||
assert.True(t, input.Person.IsSet())
|
||||
assert.True(t, input.Person.IsNull())
|
||||
})
|
||||
}
|
||||
|
||||
func TestOmittable_Pointers(t *testing.T) {
|
||||
type Input struct {
|
||||
Name Omittable[*string] `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
t.Run("with value", func(t *testing.T) {
|
||||
jsonData := `{"name": "hello"}`
|
||||
var input Input
|
||||
require.NoError(t, json.Unmarshal([]byte(jsonData), &input))
|
||||
|
||||
assert.True(t, input.Name.IsSet())
|
||||
value, ok := input.Name.Value()
|
||||
assert.True(t, ok)
|
||||
require.NotNil(t, value)
|
||||
assert.Equal(t, "hello", *value)
|
||||
})
|
||||
|
||||
t.Run("with null", func(t *testing.T) {
|
||||
jsonData := `{"name": null}`
|
||||
var input Input
|
||||
require.NoError(t, json.Unmarshal([]byte(jsonData), &input))
|
||||
|
||||
assert.True(t, input.Name.IsSet())
|
||||
assert.True(t, input.Name.IsNull())
|
||||
})
|
||||
}
|
||||
63
third_party/mcpgen/mcp/recover.go
vendored
63
third_party/mcpgen/mcp/recover.go
vendored
@@ -1,63 +0,0 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
// RecoverFunc is called when a tool handler panics. It receives the recovered
|
||||
// value (whatever was passed to panic) and returns an error to be reported to
|
||||
// the client.
|
||||
//
|
||||
// This matches the signature and semantics of gqlgen's RecoverFunc.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// server.New(resolver, server.WithRecoverFunc(func(ctx context.Context, err any) error {
|
||||
// log.Error("tool panic", "err", err)
|
||||
// return errors.New("internal server error")
|
||||
// }))
|
||||
type RecoverFunc func(ctx context.Context, err any) error
|
||||
|
||||
// DefaultRecoverFunc prints the panic and stack trace to stderr and returns a
|
||||
// generic internal error. This matches gqlgen's DefaultRecover behavior.
|
||||
func DefaultRecoverFunc(_ context.Context, err any) error {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
fmt.Fprintln(os.Stderr)
|
||||
debug.PrintStack()
|
||||
return errors.New("internal system error")
|
||||
}
|
||||
|
||||
// Option configures the generated MCP server.
|
||||
type Option func(*Options)
|
||||
|
||||
// Options holds configuration for the generated MCP server.
|
||||
type Options struct {
|
||||
RecoverFunc RecoverFunc
|
||||
}
|
||||
|
||||
// WithRecoverFunc sets the panic recover function for tool handlers.
|
||||
// The recover function is called when a tool handler panics, and its return
|
||||
// value is sent to the client in place of the panic.
|
||||
func WithRecoverFunc(fn RecoverFunc) Option {
|
||||
return func(o *Options) {
|
||||
o.RecoverFunc = fn
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyOptions applies the given options to an Options struct.
|
||||
// If RecoverFunc is nil after applying options, it is set to DefaultRecoverFunc:
|
||||
// recovery is always enabled, matching gqlgen's behavior.
|
||||
func ApplyOptions(opts []Option) Options {
|
||||
var o Options
|
||||
for _, opt := range opts {
|
||||
opt(&o)
|
||||
}
|
||||
if o.RecoverFunc == nil {
|
||||
o.RecoverFunc = DefaultRecoverFunc
|
||||
}
|
||||
return o
|
||||
}
|
||||
51
third_party/mcpgen/mcp/recover_test.go
vendored
51
third_party/mcpgen/mcp/recover_test.go
vendored
@@ -1,51 +0,0 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestApplyOptions(t *testing.T) {
|
||||
t.Run("no options uses default recover func", func(t *testing.T) {
|
||||
opts := ApplyOptions(nil)
|
||||
assert.NotNil(t, opts.RecoverFunc)
|
||||
})
|
||||
|
||||
t.Run("nil recover func falls back to default", func(t *testing.T) {
|
||||
opts := ApplyOptions([]Option{WithRecoverFunc(nil)})
|
||||
assert.NotNil(t, opts.RecoverFunc)
|
||||
})
|
||||
|
||||
t.Run("with custom recover func", func(t *testing.T) {
|
||||
fn := func(_ context.Context, _ any) error {
|
||||
return errors.New("sanitized")
|
||||
}
|
||||
opts := ApplyOptions([]Option{WithRecoverFunc(fn)})
|
||||
assert.NotNil(t, opts.RecoverFunc)
|
||||
|
||||
err := opts.RecoverFunc(context.Background(), "boom")
|
||||
assert.Equal(t, "sanitized", err.Error())
|
||||
})
|
||||
|
||||
t.Run("recover func receives raw panic value", func(t *testing.T) {
|
||||
var captured any
|
||||
fn := func(_ context.Context, err any) error {
|
||||
captured = err
|
||||
return nil
|
||||
}
|
||||
opts := ApplyOptions([]Option{WithRecoverFunc(fn)})
|
||||
|
||||
opts.RecoverFunc(context.Background(), 42)
|
||||
assert.Equal(t, 42, captured)
|
||||
|
||||
opts.RecoverFunc(context.Background(), "string panic")
|
||||
assert.Equal(t, "string panic", captured)
|
||||
|
||||
original := errors.New("error panic")
|
||||
opts.RecoverFunc(context.Background(), original)
|
||||
assert.Equal(t, original, captured)
|
||||
})
|
||||
}
|
||||
77
third_party/mcpgen/mcp/schema.go
vendored
77
third_party/mcpgen/mcp/schema.go
vendored
@@ -1,77 +0,0 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/google/jsonschema-go/jsonschema"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
// MustUnmarshalSchema unmarshals a JSON schema string into a jsonschema.Schema
|
||||
// Panics if unmarshaling fails, providing compile-time safety for schema definitions
|
||||
func MustUnmarshalSchema(schemaJSON string) *jsonschema.Schema {
|
||||
var schema jsonschema.Schema
|
||||
if err := json.Unmarshal([]byte(schemaJSON), &schema); err != nil {
|
||||
panic("invalid schema JSON: " + err.Error())
|
||||
}
|
||||
return &schema
|
||||
}
|
||||
|
||||
// PromptHandlerFor is a typed prompt handler that accepts structured arguments.
|
||||
// Similar to mcp.ToolHandlerFor, this allows prompts to work with typed Go structs
|
||||
// instead of raw map[string]string.
|
||||
//
|
||||
// The Args type parameter must be a struct or map type. Arguments will be automatically
|
||||
// unmarshaled from the prompt request's Arguments map into the Args type.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type TaskArgs struct {
|
||||
// Topic string `json:"topic"`
|
||||
// Detailed bool `json:"detailed"`
|
||||
// }
|
||||
//
|
||||
// func (r *Resolver) TaskHelpPrompt(ctx context.Context, req *mcp.GetPromptRequest, args TaskArgs) (*mcp.GetPromptResult, error) {
|
||||
// // args.Topic and args.Detailed are already parsed
|
||||
// return &mcp.GetPromptResult{...}, nil
|
||||
// }
|
||||
type PromptHandlerFor[Args any] func(context.Context, *mcp.GetPromptRequest, Args) (*mcp.GetPromptResult, error)
|
||||
|
||||
// AddPrompt is a generic wrapper around Server.AddPrompt that provides type-safe argument handling.
|
||||
// It automatically converts the prompt arguments from map[string]string into the typed Args parameter.
|
||||
//
|
||||
// This matches the ergonomics of mcp.AddTool for a consistent API experience across tools and prompts.
|
||||
//
|
||||
// The Args type must be a struct with string fields or map[string]string. If it's a struct, the fields
|
||||
// will be populated from the arguments map based on their json tags.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type HelpArgs struct {
|
||||
// Topic string `json:"topic"`
|
||||
// }
|
||||
//
|
||||
// mcp.AddPrompt(server, &mcp.Prompt{
|
||||
// Name: "help",
|
||||
// Description: "Get help",
|
||||
// }, resolver.HelpPrompt) // HelpPrompt receives typed HelpArgs
|
||||
func AddPrompt[Args any](s *mcp.Server, p *mcp.Prompt, h PromptHandlerFor[Args]) {
|
||||
s.AddPrompt(p, func(ctx context.Context, req *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
|
||||
var args Args
|
||||
|
||||
// Convert map[string]string to typed Args using JSON as the intermediary.
|
||||
// This properly handles json tags and field mapping.
|
||||
if len(req.Params.Arguments) > 0 {
|
||||
argsBytes, err := json.Marshal(req.Params.Arguments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := json.Unmarshal(argsBytes, &args); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return h(ctx, req, args)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user