feat(mcp): cap MCP batch tool at MAX_BATCH_OPERATIONS (PRJ-22)
- enforce the same 100-operation cap the HTTP /batch endpoint enforces; over-cap \x60calls\x60 reject with a structured INVALID_INPUT (retryable: false, matching CallError::invalid_input) before any dispatch - hoist MAX_BATCH_OPERATIONS to gateway/mod.rs and reuse it in routes, to_openapi (removing a pre-existing duplicate literal), and to_mcp - state the limit in the batch tool description and add maxItems to the input schema (doc previously advertised no limit) - GatewayDispatch gains a per-instance invoke_count spy accessor so the over-cap test proves zero dispatches (process-global counters raced under the parallel test runner) - tests: over-cap -> INVALID_INPUT + invoke_count()==0; at-cap -> 100 results + invoke_count()==100 verification: scripts/verify.sh (352 passed) and scripts/verify.sh --all-features (468 passed); cargo clippy --all-targets -D warnings and cargo fmt --check clean
This commit is contained in:
@@ -30,6 +30,7 @@
|
||||
//! fixed window).
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
@@ -50,6 +51,7 @@ const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
pub struct GatewayDispatch {
|
||||
registry: Arc<OperationRegistry>,
|
||||
identity_provider: Arc<dyn IdentityProvider>,
|
||||
invoke_count: AtomicUsize,
|
||||
}
|
||||
|
||||
impl GatewayDispatch {
|
||||
@@ -62,6 +64,7 @@ impl GatewayDispatch {
|
||||
Self {
|
||||
registry,
|
||||
identity_provider,
|
||||
invoke_count: AtomicUsize::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +78,14 @@ impl GatewayDispatch {
|
||||
&self.identity_provider
|
||||
}
|
||||
|
||||
/// How many [`GatewayDispatch::invoke`] calls this spine has
|
||||
/// served. A test-spy accessor: the over-cap batch tests assert it
|
||||
/// stays at zero to prove no dispatch happened before the cap
|
||||
/// rejection (review-002 PRJ-22).
|
||||
pub fn invoke_count(&self) -> usize {
|
||||
self.invoke_count.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Resolve a bearer token to an identity (the auth-middleware hook).
|
||||
pub fn resolve_bearer(&self, token: &AuthToken) -> Option<Identity> {
|
||||
self.identity_provider.resolve_from_token(token)
|
||||
@@ -88,6 +99,7 @@ impl GatewayDispatch {
|
||||
op: &str,
|
||||
input: Value,
|
||||
) -> ResponseEnvelope {
|
||||
self.invoke_count.fetch_add(1, Ordering::Relaxed);
|
||||
let operation_name = strip_leading_slash(op).to_string();
|
||||
let request_id = uuid::Uuid::new_v4().to_string();
|
||||
let context = self.build_root_context(&request_id, &operation_name, identity);
|
||||
|
||||
@@ -5,3 +5,10 @@ pub(crate) mod schema_cache;
|
||||
|
||||
pub use dispatch::GatewayDispatch;
|
||||
pub use routes::CallRequest;
|
||||
|
||||
/// The maximum number of operations accepted in a single batch,
|
||||
/// enforced identically by the HTTP `POST /batch` endpoint, the
|
||||
/// `to_openapi` gateway-spec projection, and the MCP `batch` tool
|
||||
/// (review-002 PRJ-22). One shared constant so the surfaces cannot
|
||||
/// drift.
|
||||
pub const MAX_BATCH_OPERATIONS: usize = 100;
|
||||
|
||||
@@ -45,12 +45,12 @@ use serde_json::{json, Value};
|
||||
use super::dispatch::GatewayDispatch;
|
||||
use super::error::call_error_to_http_response_with_identity;
|
||||
use super::schema_cache::{CompileFailed, PublishSchemaCache};
|
||||
use super::MAX_BATCH_OPERATIONS;
|
||||
use crate::server::auth::ResolvedIdentity;
|
||||
use crate::server::state::RouterState;
|
||||
|
||||
const SERVICES_LIST: &str = "services/list";
|
||||
const SERVICES_SCHEMA: &str = "services/schema";
|
||||
const MAX_BATCH_OPERATIONS: usize = 100;
|
||||
const MAX_PUBLISH_LINE_BYTES: usize = 2 * 1024 * 1024;
|
||||
|
||||
/// The explicit request-body limit for the whole gateway router
|
||||
|
||||
Reference in New Issue
Block a user