mcp接口增加分页
This commit is contained in:
1 parent
7243319bbb
commit
7f4adad7b1
16 files changed
+1157
-281
No files matched your search
+110
-61
@@ -38,13 +38,13 @@ const (
|
||||
|
||||
// MCPSpatialService is the use-case surface exposed through the MCP handler.
|
||||
type MCPSpatialService interface {
|
||||
SearchPlaceCandidates(context.Context, string, int) (service.QueryResult[[]domain.PlaceCandidate], error)
|
||||
ResolveIncidentContext(context.Context, domain.Coordinate) (service.QueryResult[[]domain.IncidentContext], error)
|
||||
FindNearbyWaterSources(context.Context, domain.Coordinate, float64, int) (service.QueryResult[[]domain.WaterSource], error)
|
||||
FindCommandPostCandidates(context.Context, domain.Coordinate, float64, int) (service.QueryResult[[]domain.CommandPostCandidate], error)
|
||||
ListNearbyAccessLines(context.Context, domain.Coordinate, float64, int) (service.QueryResult[[]domain.AccessLine], error)
|
||||
GetResponsibleUnits(context.Context, domain.Coordinate) (service.QueryResult[[]domain.ResponsibleUnit], error)
|
||||
FindNearbyRiskAreas(context.Context, domain.Coordinate, float64, int) (service.QueryResult[[]domain.RiskArea], error)
|
||||
SearchPlaceCandidates(context.Context, string, int, int) (service.QueryResult[[]domain.PlaceCandidate], error)
|
||||
ResolveIncidentContext(context.Context, domain.Coordinate, int, int) (service.QueryResult[[]domain.IncidentContext], error)
|
||||
FindNearbyWaterSources(context.Context, domain.Coordinate, float64, int, int) (service.QueryResult[[]domain.WaterSource], error)
|
||||
FindCommandPostCandidates(context.Context, domain.Coordinate, float64, int, int) (service.QueryResult[[]domain.CommandPostCandidate], error)
|
||||
ListNearbyAccessLines(context.Context, domain.Coordinate, float64, int, int) (service.QueryResult[[]domain.AccessLine], error)
|
||||
GetResponsibleUnits(context.Context, domain.Coordinate, int, int) (service.QueryResult[[]domain.ResponsibleUnit], error)
|
||||
FindNearbyRiskAreas(context.Context, domain.Coordinate, float64, int, int) (service.QueryResult[[]domain.RiskArea], error)
|
||||
}
|
||||
|
||||
// MCPOptions contains the independently authenticated MCP transport settings.
|
||||
@@ -215,68 +215,66 @@ func (h *MCPHandler) handleToolCall(w http.ResponseWriter, r *http.Request, requ
|
||||
func (h *MCPHandler) callTool(ctx context.Context, name string, arguments json.RawMessage) (any, error) {
|
||||
switch name {
|
||||
case toolSearchPlaceCandidates:
|
||||
placeName, limit, err := decodePlaceSearchArguments(arguments)
|
||||
placeName, limit, offset, err := decodePlaceSearchArguments(arguments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return h.service.SearchPlaceCandidates(ctx, placeName, limit)
|
||||
return h.service.SearchPlaceCandidates(ctx, placeName, limit, offset)
|
||||
case toolResolveIncidentContext:
|
||||
point, err := decodePointArguments(arguments)
|
||||
point, limit, offset, err := decodePointArguments(arguments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return h.service.ResolveIncidentContext(ctx, point)
|
||||
return h.service.ResolveIncidentContext(ctx, point, limit, offset)
|
||||
case toolFindNearbyWaterSources:
|
||||
point, radius, limit, err := decodeNearbyArguments(arguments, 30_000)
|
||||
point, radius, limit, offset, err := decodeNearbyArguments(arguments, 30_000)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return h.service.FindNearbyWaterSources(ctx, point, radius, limit)
|
||||
return h.service.FindNearbyWaterSources(ctx, point, radius, limit, offset)
|
||||
case toolFindCommandPostCandidates:
|
||||
point, radius, limit, err := decodeNearbyArguments(arguments, 20_000)
|
||||
point, radius, limit, offset, err := decodeNearbyArguments(arguments, 20_000)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return h.service.FindCommandPostCandidates(ctx, point, radius, limit)
|
||||
return h.service.FindCommandPostCandidates(ctx, point, radius, limit, offset)
|
||||
case toolListNearbyAccessLines:
|
||||
point, radius, limit, err := decodeNearbyArguments(arguments, 10_000)
|
||||
point, radius, limit, offset, err := decodeNearbyArguments(arguments, 10_000)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return h.service.ListNearbyAccessLines(ctx, point, radius, limit)
|
||||
return h.service.ListNearbyAccessLines(ctx, point, radius, limit, offset)
|
||||
case toolGetResponsibleUnits:
|
||||
point, err := decodePointArguments(arguments)
|
||||
point, limit, offset, err := decodePointArguments(arguments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return h.service.GetResponsibleUnits(ctx, point)
|
||||
return h.service.GetResponsibleUnits(ctx, point, limit, offset)
|
||||
case toolFindNearbyRiskAreas:
|
||||
point, radius, limit, err := decodeNearbyArguments(arguments, 10_000)
|
||||
point, radius, limit, offset, err := decodeNearbyArguments(arguments, 10_000)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return h.service.FindNearbyRiskAreas(ctx, point, radius, limit)
|
||||
return h.service.FindNearbyRiskAreas(ctx, point, radius, limit, offset)
|
||||
default:
|
||||
return nil, errToolNotFound
|
||||
}
|
||||
}
|
||||
|
||||
func decodePlaceSearchArguments(raw json.RawMessage) (string, int, error) {
|
||||
func decodePlaceSearchArguments(raw json.RawMessage) (string, int, int, error) {
|
||||
var arguments struct {
|
||||
PlaceName *string `json:"place_name"`
|
||||
Limit *int `json:"limit"`
|
||||
Offset *int `json:"offset"`
|
||||
}
|
||||
if err := decodeStrictObject(raw, &arguments); err != nil || arguments.PlaceName == nil {
|
||||
return "", 0, fmt.Errorf("%w: place_name is required and unknown fields are not allowed", service.ErrInvalidArgument)
|
||||
return "", 0, 0, fmt.Errorf("%w: place_name is required and unknown fields are not allowed", service.ErrInvalidArgument)
|
||||
}
|
||||
var limit int
|
||||
if arguments.Limit != nil {
|
||||
limit = *arguments.Limit
|
||||
if limit < 1 || limit > 20 {
|
||||
return "", 0, fmt.Errorf("%w: limit must be between 1 and 20", service.ErrInvalidArgument)
|
||||
}
|
||||
limit, offset, err := decodePaginationArguments(arguments.Limit, arguments.Offset)
|
||||
if err != nil {
|
||||
return "", 0, 0, err
|
||||
}
|
||||
return *arguments.PlaceName, limit, nil
|
||||
return *arguments.PlaceName, limit, offset, nil
|
||||
}
|
||||
|
||||
func (h *MCPHandler) writeToolResult(w http.ResponseWriter, id json.RawMessage, output any, isError bool) {
|
||||
@@ -400,42 +398,64 @@ func validRPCID(id json.RawMessage) bool {
|
||||
return decoder.Decode(&value) == nil
|
||||
}
|
||||
|
||||
func decodePointArguments(raw json.RawMessage) (domain.Coordinate, error) {
|
||||
func decodePointArguments(raw json.RawMessage) (domain.Coordinate, int, int, error) {
|
||||
var arguments struct {
|
||||
Longitude *float64 `json:"longitude"`
|
||||
Latitude *float64 `json:"latitude"`
|
||||
Limit *int `json:"limit"`
|
||||
Offset *int `json:"offset"`
|
||||
}
|
||||
if err := decodeStrictObject(raw, &arguments); err != nil || arguments.Longitude == nil || arguments.Latitude == nil {
|
||||
return domain.Coordinate{}, fmt.Errorf("%w: longitude and latitude are required", service.ErrInvalidArgument)
|
||||
return domain.Coordinate{}, 0, 0, fmt.Errorf("%w: longitude and latitude are required and unknown fields are not allowed", service.ErrInvalidArgument)
|
||||
}
|
||||
return domain.Coordinate{Longitude: *arguments.Longitude, Latitude: *arguments.Latitude}, nil
|
||||
limit, offset, err := decodePaginationArguments(arguments.Limit, arguments.Offset)
|
||||
if err != nil {
|
||||
return domain.Coordinate{}, 0, 0, err
|
||||
}
|
||||
return domain.Coordinate{Longitude: *arguments.Longitude, Latitude: *arguments.Latitude}, limit, offset, nil
|
||||
}
|
||||
|
||||
func decodeNearbyArguments(raw json.RawMessage, maximumRadius float64) (domain.Coordinate, float64, int, error) {
|
||||
func decodeNearbyArguments(raw json.RawMessage, maximumRadius float64) (domain.Coordinate, float64, int, int, error) {
|
||||
var arguments struct {
|
||||
Longitude *float64 `json:"longitude"`
|
||||
Latitude *float64 `json:"latitude"`
|
||||
RadiusMeters *float64 `json:"radius_meters"`
|
||||
Limit *int `json:"limit"`
|
||||
Offset *int `json:"offset"`
|
||||
}
|
||||
if err := decodeStrictObject(raw, &arguments); err != nil || arguments.Longitude == nil || arguments.Latitude == nil {
|
||||
return domain.Coordinate{}, 0, 0, fmt.Errorf("%w: longitude and latitude are required and unknown fields are not allowed", service.ErrInvalidArgument)
|
||||
return domain.Coordinate{}, 0, 0, 0, fmt.Errorf("%w: longitude and latitude are required and unknown fields are not allowed", service.ErrInvalidArgument)
|
||||
}
|
||||
var radius float64
|
||||
if arguments.RadiusMeters != nil {
|
||||
radius = *arguments.RadiusMeters
|
||||
if radius < 100 || radius > maximumRadius {
|
||||
return domain.Coordinate{}, 0, 0, fmt.Errorf("%w: radius_meters is outside the tool limit", service.ErrInvalidArgument)
|
||||
return domain.Coordinate{}, 0, 0, 0, fmt.Errorf("%w: radius_meters is outside the tool limit", service.ErrInvalidArgument)
|
||||
}
|
||||
}
|
||||
var limit int
|
||||
if arguments.Limit != nil {
|
||||
limit = *arguments.Limit
|
||||
if limit < 1 || limit > 20 {
|
||||
return domain.Coordinate{}, 0, 0, fmt.Errorf("%w: limit must be between 1 and 20", service.ErrInvalidArgument)
|
||||
limit, offset, err := decodePaginationArguments(arguments.Limit, arguments.Offset)
|
||||
if err != nil {
|
||||
return domain.Coordinate{}, 0, 0, 0, err
|
||||
}
|
||||
return domain.Coordinate{Longitude: *arguments.Longitude, Latitude: *arguments.Latitude}, radius, limit, offset, nil
|
||||
}
|
||||
|
||||
func decodePaginationArguments(limit, offset *int) (int, int, error) {
|
||||
decodedLimit := 0
|
||||
if limit != nil {
|
||||
decodedLimit = *limit
|
||||
if decodedLimit < 1 || decodedLimit > 20 {
|
||||
return 0, 0, fmt.Errorf("%w: limit must be between 1 and 20", service.ErrInvalidArgument)
|
||||
}
|
||||
}
|
||||
return domain.Coordinate{Longitude: *arguments.Longitude, Latitude: *arguments.Latitude}, radius, limit, nil
|
||||
decodedOffset := 0
|
||||
if offset != nil {
|
||||
decodedOffset = *offset
|
||||
if decodedOffset < 0 || decodedOffset > service.MaxPageOffset {
|
||||
return 0, 0, fmt.Errorf("%w: offset must be between 0 and %d", service.ErrInvalidArgument, service.MaxPageOffset)
|
||||
}
|
||||
}
|
||||
return decodedLimit, decodedOffset, nil
|
||||
}
|
||||
|
||||
func decodeStrictObject(raw json.RawMessage, target any) error {
|
||||
@@ -480,6 +500,11 @@ func toolErrorPayload(code, message string) map[string]any {
|
||||
"data_sources": []string{},
|
||||
"spatial_reference": "EPSG:4326",
|
||||
"result_count": 0,
|
||||
"total_count": 0,
|
||||
"limit": 0,
|
||||
"offset": 0,
|
||||
"has_more": false,
|
||||
"next_offset": nil,
|
||||
},
|
||||
"warnings": []string{},
|
||||
"error": map[string]string{
|
||||
@@ -630,6 +655,7 @@ func mcpToolDefinitions() []mcpToolDefinition {
|
||||
}
|
||||
|
||||
func placeSearchInputSchema() map[string]any {
|
||||
pagination := paginationInputProperties(10)
|
||||
return map[string]any{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
@@ -640,13 +666,8 @@ func placeSearchInputSchema() map[string]any {
|
||||
"maxLength": 100,
|
||||
"description": "Town, village, grid, facility, water source, access line, or risk-area name",
|
||||
},
|
||||
"limit": map[string]any{
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 20,
|
||||
"default": 10,
|
||||
"description": "Maximum number of candidates",
|
||||
},
|
||||
"limit": pagination["limit"],
|
||||
"offset": pagination["offset"],
|
||||
},
|
||||
"required": []string{"place_name"},
|
||||
}
|
||||
@@ -662,12 +683,15 @@ func readOnlyAnnotations() map[string]any {
|
||||
}
|
||||
|
||||
func pointInputSchema() map[string]any {
|
||||
pagination := paginationInputProperties(20)
|
||||
return map[string]any{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": map[string]any{
|
||||
"longitude": numberSchema("WGS84 longitude", -180, 180),
|
||||
"latitude": numberSchema("WGS84 latitude", -90, 90),
|
||||
"limit": pagination["limit"],
|
||||
"offset": pagination["offset"],
|
||||
},
|
||||
"required": []string{"longitude", "latitude"},
|
||||
}
|
||||
@@ -675,6 +699,7 @@ func pointInputSchema() map[string]any {
|
||||
|
||||
func nearbyInputSchema(maximumRadius, defaultRadius float64) map[string]any {
|
||||
properties := pointInputSchema()["properties"].(map[string]any)
|
||||
pagination := paginationInputProperties(10)
|
||||
properties["radius_meters"] = map[string]any{
|
||||
"type": "number",
|
||||
"minimum": 100,
|
||||
@@ -682,13 +707,8 @@ func nearbyInputSchema(maximumRadius, defaultRadius float64) map[string]any {
|
||||
"default": defaultRadius,
|
||||
"description": "Search radius in meters",
|
||||
}
|
||||
properties["limit"] = map[string]any{
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 20,
|
||||
"default": 10,
|
||||
"description": "Maximum number of results",
|
||||
}
|
||||
properties["limit"] = pagination["limit"]
|
||||
properties["offset"] = pagination["offset"]
|
||||
return map[string]any{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
@@ -697,6 +717,25 @@ func nearbyInputSchema(maximumRadius, defaultRadius float64) map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
func paginationInputProperties(defaultLimit int) map[string]any {
|
||||
return map[string]any{
|
||||
"limit": map[string]any{
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 20,
|
||||
"default": defaultLimit,
|
||||
"description": "Maximum number of results in this page",
|
||||
},
|
||||
"offset": map[string]any{
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": service.MaxPageOffset,
|
||||
"default": 0,
|
||||
"description": "Zero-based result offset for pagination",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func numberSchema(description string, minimum, maximum float64) map[string]any {
|
||||
return map[string]any{
|
||||
"type": "number",
|
||||
@@ -729,13 +768,23 @@ func outputEnvelopeSchema(itemSchema map[string]any) map[string]any {
|
||||
"metadata": map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"generated_at": stringSchema(),
|
||||
"data_sources": map[string]any{"type": "array", "items": stringSchema()},
|
||||
"spatial_reference": stringSchema(),
|
||||
"result_count": map[string]any{"type": "integer", "minimum": 0},
|
||||
"generated_at": stringSchema(),
|
||||
"data_sources": map[string]any{"type": "array", "items": stringSchema()},
|
||||
"spatial_reference": stringSchema(),
|
||||
"result_count": map[string]any{"type": "integer", "minimum": 0},
|
||||
"total_count": map[string]any{"type": "integer", "minimum": 0},
|
||||
"limit": map[string]any{"type": "integer", "minimum": 0, "maximum": 20},
|
||||
"offset": map[string]any{"type": "integer", "minimum": 0, "maximum": service.MaxPageOffset},
|
||||
"has_more": boolSchema(),
|
||||
"next_offset": map[string]any{
|
||||
"oneOf": []map[string]any{
|
||||
{"type": "integer", "minimum": 0},
|
||||
{"type": "null"},
|
||||
},
|
||||
},
|
||||
"search_radius_meters": map[string]any{"type": "number", "minimum": 0},
|
||||
},
|
||||
"required": []string{"generated_at", "data_sources", "spatial_reference", "result_count"},
|
||||
"required": []string{"generated_at", "data_sources", "spatial_reference", "result_count", "total_count", "limit", "offset", "has_more", "next_offset"},
|
||||
},
|
||||
"warnings": map[string]any{"type": "array", "items": stringSchema()},
|
||||
"error": map[string]any{
|
||||
|
||||
Reference in new issue
Block a user