Skip to content

Feature/client level identifiers #666

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Changelog

## [Unreleased]

### Added
- Client-level request identifiers feature:
- Added optional `identifiers` field to `ClientOptions` for setting client-wide identifiers
- Added optional `identifiers` field to `CallToolRequest` schema for per-request identifiers
- Added identifier merging logic in client's `callTool` method with request-level precedence
- Added `IdentifierForwardingConfig` to `ServerOptions` for configuring identifier forwarding
- Added `forwardIdentifiersAsHeaders` method to `McpServer` for converting identifiers to HTTP headers
- Added `EnhancedRequestHandlerExtra` interface with identifiers and helper methods
- Added server-side security validation with key format and value content filtering
- Added configurable identifier limits with deterministic truncation behavior
- Added ASCII-only value validation for HTTP header safety
- Added optional whitelist filtering via `allowedKeys` configuration
- Added comprehensive test suite with 11 security and functionality test scenarios
- Added example demonstrating client-level and request-level identifiers

### Security
- Identifier forwarding is disabled by default for security
- Implemented multi-layer validation to prevent header injection attacks
- Added input sanitization for keys (alphanumeric, hyphens, underscores only)
- Added control character filtering for values
- Added configurable limits for identifier count and value length

### Developer Experience
- Zero breaking changes - fully backward compatible with existing code
- Added helper method `applyIdentifiersToRequestOptions()` for easy HTTP request enhancement
- Added rich TypeScript types with proper interface extensions
- Clean protocol design - only includes identifiers field when non-empty
92 changes: 92 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"eventsource": "^3.0.2",
"express": "^5.0.1",
"express-rate-limit": "^7.5.0",
"node-fetch": "^3.3.2",
"pkce-challenge": "^5.0.0",
"raw-body": "^3.0.0",
"zod": "^3.23.8",
Expand Down
24 changes: 23 additions & 1 deletion src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ export type ClientOptions = ProtocolOptions & {
* Capabilities to advertise as being supported by this client.
*/
capabilities?: ClientCapabilities;

/**
* Optional identifiers that will be included with all tool calls made by this client.
* These identifiers can be used for distributed tracing, multi-tenancy, or other
* cross-cutting concerns.
*/
identifiers?: Record<string, string>;
};

/**
Expand Down Expand Up @@ -93,6 +100,7 @@ export class Client<
private _instructions?: string;
private _cachedToolOutputValidators: Map<string, ValidateFunction> = new Map();
private _ajv: InstanceType<typeof Ajv>;
private _clientIdentifiers: Record<string, string>;

/**
* Initializes this client with the given name and version information.
Expand All @@ -103,6 +111,7 @@ export class Client<
) {
super(options);
this._capabilities = options?.capabilities ?? {};
this._clientIdentifiers = options?.identifiers ?? {};
this._ajv = new Ajv();
}

Expand Down Expand Up @@ -433,8 +442,21 @@ export class Client<
| typeof CompatibilityCallToolResultSchema = CallToolResultSchema,
options?: RequestOptions,
) {
// Merge client identifiers with any request-specific identifiers
// Request identifiers take precedence over client identifiers when keys conflict
const mergedIdentifiers = {
...this._clientIdentifiers,
...(params.identifiers || {}),
};

// Only include identifiers field if there are actual identifiers to send
const mergedParams = {
...params,
...(Object.keys(mergedIdentifiers).length > 0 && { identifiers: mergedIdentifiers }),
};

const result = await this.request(
{ method: "tools/call", params },
{ method: "tools/call", params: mergedParams },
resultSchema,
options,
);
Expand Down
Loading