mcp接口增加分页

This commit is contained in:
andy committed 2026-09-06 17:57:58 +08:00
1 parent 7243319bbb
commit 7f4adad7b1
16 files changed
+1157 -281

No files matched your search

+110 -61
View File
@@ -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{