Skip to content
Closed
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
7 changes: 7 additions & 0 deletions packages/agent-mesh/packages/mcp-governance/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
dist/
node_modules/
coverage/
src/**/*.js
src/**/*.js.map
src/**/*.d.ts
src/**/*.d.ts.map
92 changes: 92 additions & 0 deletions packages/agent-mesh/packages/mcp-governance/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# AgentMesh MCP Governance Primitives

> [!IMPORTANT]
> **Public Preview** — This npm package is a Microsoft-signed public preview release.
> APIs may change before GA.

Standalone MCP governance primitives for AgentMesh. Use this package when you only need MCP authentication, signing, redaction, scanning, rate limiting, and gateway enforcement without pulling in the full SDK.

## Installation

Install the standalone MCP governance package:

```bash
npm install @microsoft/agentmesh-mcp-governance
```

If you also need AgentMesh identity, trust, policy, and audit APIs, install the full SDK instead:

```bash
npm install @microsoft/agentmesh-sdk
```

## Quick Start

```typescript
import {
ApprovalStatus,
CredentialRedactor,
InMemoryMCPAuditSink,
MCPGateway,
MCPMessageSigner,
MCPResponseScanner,
MCPSecurityScanner,
MCPSessionAuthenticator,
MCPSlidingRateLimiter,
} from '@microsoft/agentmesh-mcp-governance';

const auditSink = new InMemoryMCPAuditSink();
const gateway = new MCPGateway({
allowedTools: ['read_file', 'search_docs'],
sensitiveTools: ['deploy'],
auditSink,
rateLimit: { maxRequests: 60, windowMs: 60_000 },
approvalHandler: async ({ toolName }) =>
toolName === 'deploy'
? ApprovalStatus.Approved
: ApprovalStatus.Pending,
});

const decision = await gateway.evaluateToolCall('agent-1', 'read_file', {
path: '/workspace/README.md',
});

const sessionAuth = new MCPSessionAuthenticator({
secret: process.env.MCP_SESSION_SECRET!,
});
const signer = new MCPMessageSigner({
secret: process.env.MCP_SIGNING_SECRET!,
});
const scanner = new MCPResponseScanner();
const metadataScanner = new MCPSecurityScanner();
const redactor = new CredentialRedactor();
const limiter = new MCPSlidingRateLimiter({ maxRequests: 10, windowMs: 1_000 });

void decision;
void sessionAuth;
void signer;
void scanner;
void metadataScanner;
void redactor;
void limiter;
```

## What You Get

- `MCPResponseScanner` — detects instruction-injection tags, imperative phrasing, credential leaks, and exfiltration URLs before tool output reaches an LLM
- `MCPSessionAuthenticator` — signs session tokens bound to agent identity with TTL expiry and concurrent-session enforcement
- `MCPMessageSigner` — HMAC-SHA256 payload signing with timestamp and nonce replay protection
- `CredentialRedactor` — removes credential material from strings and nested objects before logging or storage
- `MCPSlidingRateLimiter` — enforces per-agent sliding-window limits for MCP traffic
- `MCPSecurityScanner` — scans tool metadata for poisoning, rug pulls, cross-server collisions, description injection, and schema abuse
- `MCPGateway` — enforces deny-list, allow-list, sanitization, rate limiting, and human approval stages with fail-closed behavior

## Deployment Notes

- The in-memory stores are suitable for tests and single-process development.
- Production deployments should provide durable implementations for session, nonce, rate-limit, and audit storage.
- Audit entries are designed to carry redacted parameters only; metrics should use categorical labels rather than raw payloads.

## License

MIT — see [LICENSE](../../../../LICENSE).
38 changes: 38 additions & 0 deletions packages/agent-mesh/packages/mcp-governance/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
const tsParser = require('@typescript-eslint/parser');
const tsPlugin = require('@typescript-eslint/eslint-plugin');

/** @type {import('eslint').Linter.FlatConfig[]} */
module.exports = [
{
ignores: ['dist/**', 'coverage/**', 'node_modules/**'],
},
{
files: ['src/**/*.ts', 'tests/**/*.ts'],
languageOptions: {
parser: tsParser,
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
},
globals: {
Buffer: 'readonly',
console: 'readonly',
describe: 'readonly',
expect: 'readonly',
beforeEach: 'readonly',
afterEach: 'readonly',
it: 'readonly',
performance: 'readonly',
process: 'readonly',
},
},
plugins: {
'@typescript-eslint': tsPlugin,
},
rules: {
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
},
},
];
12 changes: 12 additions & 0 deletions packages/agent-mesh/packages/mcp-governance/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/tests'],
testMatch: ['**/*.test.ts'],
collectCoverageFrom: ['src/**/*.ts'],
coverageDirectory: 'coverage',
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
};
57 changes: 57 additions & 0 deletions packages/agent-mesh/packages/mcp-governance/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"name": "@microsoft/agentmesh-mcp-governance",
"version": "3.0.2",
"description": "Public Preview — Standalone MCP governance primitives for AgentMesh",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist/",
"README.md"
],
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit",
"test": "jest --config jest.config.js",
"lint": "eslint src tests",
"clean": "rimraf dist",
"prepare": "npm run build",
"prepublishOnly": "npm run build"
},
"keywords": [
"agentmesh",
"mcp",
"governance",
"security",
"rate-limiting",
"signing"
],
"author": "Microsoft Corporation",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/microsoft/agent-governance-toolkit.git",
"directory": "packages/agent-mesh/packages/mcp-governance"
},
"devDependencies": {
"typescript": "5.7.3",
"@types/node": "25.5.0",
"jest": "29.7.0",
"ts-jest": "29.2.5",
"@types/jest": "29.5.14",
"eslint": "9.0.0",
"@typescript-eslint/parser": "8.58.0",
"@typescript-eslint/eslint-plugin": "8.58.0",
"rimraf": "6.1.3"
},
"engines": {
"node": ">=18.0.0"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.js"
}
},
"homepage": "https://github.com/microsoft/agent-governance-toolkit/tree/main/packages/agent-mesh/packages/mcp-governance"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import {
CredentialPatternDefinition,
CredentialRedaction,
CredentialRedactionResult,
CredentialRedactorConfig,
} from './types';
import {
createRegexScanBudget,
isRecord,
truncatePreview,
validateRegex,
} from './utils';

const DEFAULT_REPLACEMENT = '[REDACTED]';
const SENSITIVE_KEY_PATTERN = /(password|passwd|pwd|secret|token|api[_-]?key|connection.?string|accountkey|sharedaccesssignature|sas)/i;

const BUILTIN_PATTERNS: CredentialPatternDefinition[] = [
{ name: 'openai_key', pattern: /\bsk-[A-Za-z0-9]{16,}\b/g },
{ name: 'github_token', pattern: /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g },
{ name: 'aws_access_key', pattern: /\bAKIA[0-9A-Z]{16}\b/g },
{ name: 'bearer_token', pattern: /\bBearer\s+[A-Za-z0-9._\-+/=]{10,}\b/gi },
{
name: 'connection_string',
pattern: /\b(?:AccountKey|SharedAccessKey|Password|Pwd|Secret|ApiKey)\s*=\s*[^;,\s]+/gi,
},
{
name: 'pem_block',
pattern: /-----BEGIN [A-Z0-9 ]+-----[\s\S]*?-----END [A-Z0-9 ]+-----/g,
},
];

interface CompiledPattern {
name: string;
pattern: RegExp;
replacement: string;
}

/**
* Redacts credential-like values from strings and structured objects.
*/
export class CredentialRedactor {
private readonly replacementText: string;
private readonly redactSensitiveKeys: boolean;
private readonly patterns: CompiledPattern[];
private readonly clock: CredentialRedactorConfig['clock'];
private readonly scanTimeoutMs: CredentialRedactorConfig['scanTimeoutMs'];

constructor(config: CredentialRedactorConfig = {}) {
this.replacementText = config.replacementText ?? DEFAULT_REPLACEMENT;
this.redactSensitiveKeys = config.redactSensitiveKeys ?? true;
this.clock = config.clock;
this.scanTimeoutMs = config.scanTimeoutMs;
this.patterns = [...BUILTIN_PATTERNS, ...(config.customPatterns ?? [])].map(
(definition) => ({
name: definition.name,
pattern: toGlobalPattern(definition.pattern),
replacement: definition.replacement ?? this.replacementText,
}),
);
}

redactString(
value: string,
path?: string,
): CredentialRedactionResult<string> {
const budget = createRegexScanBudget(this.clock, this.scanTimeoutMs);
return this.redactStringWithBudget(value, path, budget);
}

redact<T>(value: T): CredentialRedactionResult<T> {
const redactions: CredentialRedaction[] = [];
const seen = new WeakMap<object, unknown>();
const budget = createRegexScanBudget(this.clock, this.scanTimeoutMs);
const redacted = this.redactNode(value, '$', redactions, seen, budget) as T;
return {
redacted,
redactions,
};
}

private redactStringWithBudget(
value: string,
path: string | undefined,
budget: ReturnType<typeof createRegexScanBudget>,
): CredentialRedactionResult<string> {
let nextValue = value;
const redactions: CredentialRedaction[] = [];

for (const pattern of this.patterns) {
budget.checkpoint('Regex scan exceeded time budget - content blocked');
pattern.pattern.lastIndex = 0;
const matches = [...nextValue.matchAll(pattern.pattern)];
if (matches.length === 0) {
continue;
}

for (const match of matches) {
redactions.push({
type: pattern.name,
path,
replacement: pattern.replacement,
matchedValueType: pattern.name,
matchedTextPreview: truncatePreview(match[0]),
});
}

nextValue = nextValue.replace(pattern.pattern, pattern.replacement);
}

return {
redacted: nextValue,
redactions,
};
}

private redactNode(
value: unknown,
path: string,
redactions: CredentialRedaction[],
seen: WeakMap<object, unknown>,
budget: ReturnType<typeof createRegexScanBudget>,
): unknown {
if (typeof value === 'string') {
budget.checkpoint('Regex scan exceeded time budget - content blocked');
const result = this.redactStringWithBudget(value, path, budget);
redactions.push(...result.redactions);
return result.redacted;
}

if (Array.isArray(value)) {
return value.map((item, index) =>
this.redactNode(item, `${path}[${index}]`, redactions, seen, budget),
);
}

if (!isRecord(value)) {
return value;
}

if (seen.has(value)) {
return seen.get(value);
}

const clone: Record<string, unknown> = {};
seen.set(value, clone);

for (const [key, current] of Object.entries(value)) {
const childPath = `${path}.${key}`;
if (
this.redactSensitiveKeys
&& SENSITIVE_KEY_PATTERN.test(key)
&& typeof current === 'string'
) {
redactions.push({
type: 'sensitive_key',
path: childPath,
replacement: this.replacementText,
matchedValueType: 'sensitive_key',
matchedTextPreview: truncatePreview(current),
});
clone[key] = this.replacementText;
continue;
}

clone[key] = this.redactNode(current, childPath, redactions, seen, budget);
}

return clone;
}
}

function toGlobalPattern(pattern: RegExp | string): RegExp {
const compiled = pattern instanceof RegExp
? new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`)
: new RegExp(pattern, 'g');
validateRegex(compiled);

if (pattern instanceof RegExp) {
return compiled;
}
return compiled;
}
Loading
Loading