Summary
The docs show context.input being read inside middleware, and an interceptor example keyed on context.metadata?.input — but ExecutionContext has no input field and the framework never populates one. Tool arguments are reachable only from Pipes and the tool handler, never from a guard, middleware, or interceptor.
The practical effect is that there is no argument-aware around extension point — which is the classic cross-cutting use case (auditing, authorization, caching keyed on inputs).
Environment
- NitroStack package(s) and version(s):
@nitrostack/core@1.0.14 (also verified against main), @nitrostack/cli@1.0.15, @nitrostack/widgets@1.0.8
- Node.js version (
node -v): v24.18.0
- npm version (
npm -v): 11.16.0
- OS and version: macOS 26.2
Steps To Reproduce
- Create an interceptor (or middleware) that tries to read the tool's arguments from
ExecutionContext.
- Bind it to a tool with
@UseInterceptors(...) / @UseMiddleware(...).
- Call the tool with any arguments.
Expected Behavior
Per the SDK reference, context.input contains the tool's arguments, so a middleware/interceptor can inspect what it is wrapping.
Actual Behavior
context.input is undefined. ExecutionContext contains only requestId, toolName, logger, metadata, auth, and task. context.metadata is the MCP _meta field, so it is {} unless a client happens to send _meta.
Minimal Reproduction
@Interceptor()
export class ArgsInterceptor implements InterceptorInterface {
async intercept(context: ExecutionContext, next: () => Promise<unknown>): Promise<unknown> {
console.log('input:', (context as any).input); // undefined
console.log('metadata:', context.metadata); // {} unless the client sent _meta
return next();
}
}
Where the documentation says otherwise
Three separate surfaces describe this capability:
- SDK reference (middleware) —
context.logger.info('Tool starting', { tool: context.toolName, input: context.input })
- Interceptors guide — a
CacheInterceptor example keying its cache on context.metadata?.input
- Official agent skill
middleware-pipeline — "Interceptors can transform/intercept input arguments or mapped output from a tool method execution." (the interface shown directly beneath it is intercept(context, next))
The context.input example has also been copied into the sample apps, so the pattern is propagating. Worth noting the skills are consumed by AI coding assistants, which will confidently generate interceptors that read context.input.
Additional Context
Why the framework cannot provide it today (all in the published package, and unchanged on main):
server.js — createContext() returns { logger, requestId, toolName, metadata }
server.js — tool.execute(args, context): arguments are passed as a separate parameter
tool.js — pipeline is Guards → Middleware → Interceptors → Pipes → Handler; only executeWithPipes() and the handler ever receive the input
server.js — const { _meta, ...toolArgs } = args; createContext({ metadata: _meta }), so metadata.input only exists if a client sends it
Impact for us. We are building an approval-gate interceptor: it pauses a sensitive tool call, asks a human to confirm out-of-band, and only calls next() once approved. Without access to the arguments we cannot:
- describe the action to the human (our confirmation card reads "Book flight" rather than "SFO→JFK · $168.45"), or
- derive a per-action idempotency key — so a repeat call is indistinguishable from a retry of the previous one, which is a correctness problem for approval/authorization interceptors.
Pipes do receive the arguments, but they get no ExecutionContext (no auth, no task, no requestId) and cannot wrap the handler, so they can't serve as around-advice.
Suggested fix: attach input to the ExecutionContext handed to the pipeline, or thread it into the use() / intercept() signatures. Either would satisfy the already-documented behavior. Happy to open a PR if you have a preferred shape.
Summary
The docs show
context.inputbeing read inside middleware, and an interceptor example keyed oncontext.metadata?.input— butExecutionContexthas noinputfield and the framework never populates one. Tool arguments are reachable only from Pipes and the tool handler, never from a guard, middleware, or interceptor.The practical effect is that there is no argument-aware around extension point — which is the classic cross-cutting use case (auditing, authorization, caching keyed on inputs).
Environment
@nitrostack/core@1.0.14(also verified againstmain),@nitrostack/cli@1.0.15,@nitrostack/widgets@1.0.8node -v): v24.18.0npm -v): 11.16.0Steps To Reproduce
ExecutionContext.@UseInterceptors(...)/@UseMiddleware(...).Expected Behavior
Per the SDK reference,
context.inputcontains the tool's arguments, so a middleware/interceptor can inspect what it is wrapping.Actual Behavior
context.inputisundefined.ExecutionContextcontains onlyrequestId,toolName,logger,metadata,auth, andtask.context.metadatais the MCP_metafield, so it is{}unless a client happens to send_meta.Minimal Reproduction
Where the documentation says otherwise
Three separate surfaces describe this capability:
context.logger.info('Tool starting', { tool: context.toolName, input: context.input })CacheInterceptorexample keying its cache oncontext.metadata?.inputmiddleware-pipeline— "Interceptors can transform/intercept input arguments or mapped output from a tool method execution." (the interface shown directly beneath it isintercept(context, next))The
context.inputexample has also been copied into the sample apps, so the pattern is propagating. Worth noting the skills are consumed by AI coding assistants, which will confidently generate interceptors that readcontext.input.Additional Context
Why the framework cannot provide it today (all in the published package, and unchanged on
main):server.js—createContext()returns{ logger, requestId, toolName, metadata }server.js—tool.execute(args, context): arguments are passed as a separate parametertool.js— pipeline isGuards → Middleware → Interceptors → Pipes → Handler; onlyexecuteWithPipes()and the handler ever receive the inputserver.js—const { _meta, ...toolArgs } = args; createContext({ metadata: _meta }), sometadata.inputonly exists if a client sends itImpact for us. We are building an approval-gate interceptor: it pauses a sensitive tool call, asks a human to confirm out-of-band, and only calls
next()once approved. Without access to the arguments we cannot:Pipes do receive the arguments, but they get no
ExecutionContext(noauth, notask, norequestId) and cannot wrap the handler, so they can't serve as around-advice.Suggested fix: attach
inputto theExecutionContexthanded to the pipeline, or thread it into theuse()/intercept()signatures. Either would satisfy the already-documented behavior. Happy to open a PR if you have a preferred shape.