Sections in this lesson

MCP & Connector Security

An MCP client name, server description or declared capability is not authenticated permission.

Reviewed: · Content owner: Interview Preparator

What you will learn

  • Protocol metadata is not identity
  • Prevent confused-deputy access
  • Manage connector lifetime

Before you start

The flow

  1. 1.Identify scope
  2. 2.Inspect evidence
  3. 3.Apply host control
  4. 4.Run negative cases
  5. 5.Compare benign behavior
  6. 6.Review remaining risks

Protocol metadata is not identity

An MCP client name, server description or declared capability is not authenticated permission. Pin the protocol/documentation version and distinguish host policy from transport/authentication responsibilities.

The trustedToken fixture evaluates issuer, audience, tenant, scope and expiry only after an adapter has cryptographically verified the token. It must never receive merely decoded attacker-controlled claims as trusted input.

Prevent confused-deputy access

A connector acting with broad credentials can unintentionally serve a narrower caller. Bind the resource and delegated scope to the authenticated request, avoid token passthrough to unintended audiences and minimize granted permissions.

Inspect tool metadata and outputs as untrusted data. Reauthorize calls even when a familiar connector proposes them; a discovered tool is not automatically approved for execution.

Manage connector lifetime

Track installation source, owner, scopes, grant expiry and revocation. Changing a tool schema or destination should trigger review instead of silently inheriting an old approval.

This lesson uses an auth-policy fixture, not a deployed MCP OAuth server or conformance suite. The official security guide and actual selected SDK/transport need separate integration checks.

Try it

policy.mjs
javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { createHash, timingSafeEqual } from 'node:crypto';
export function payloadDigest(call) {
// Canonical ordered fields; extra or ambiguous fields cannot bypass payload binding.
if (!call || Object.keys(call).sort().join(',') !== 'action,resource' || !['readPlan','completePlan'].includes(call.action)
|| !/^PLAN-[0-9]+$/.test(call.resource)) throw new Error('Invalid call');
return createHash('sha256').update(JSON.stringify([call.action,call.resource])).digest('hex');
}
export function authorize(scope, call, resource, grant, now, epoch = 1) {
const digest = payloadDigest(call);
if (!Number.isFinite(now) || !Number.isSafeInteger(epoch)) return 'DENY';
if (!scope || !resource || resource.id !== call.resource || resource.tenant !== scope.tenant || !resource.readers.includes(scope.user)) return 'DENY';
if (call.action === 'readPlan') return 'ALLOW';
// Grant comes from a trusted server-side approval store, never from model output.
if (!grant || !Number.isFinite(grant.expiresAt) || grant.revoked || grant.epoch !== epoch || grant.expiresAt <= now || grant.tenant !== scope.tenant
|| grant.user !== scope.user || grant.runId !== scope.runId || grant.digest !== digest) return 'APPROVAL_REQUIRED';
return 'ALLOW';
}
export function escapedText(value) {
if (typeof value !== 'string' || value.length > 4000) throw new Error('Invalid output text');
return value.replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;').replaceAll('"','&quot;').replaceAll("'",'&#39;');
}
export function allowedUrl(value, hosts) {
try { const url = new URL(value); return url.protocol === 'https:' && !url.username && !url.password && url.port === '' && hosts.includes(url.hostname); }
catch { return false; }
}
export function trustedToken(claims, policy, now) {
// Claims must already be cryptographically verified by an auth adapter.
return claims?.issuer === policy.issuer && claims?.audience === policy.audience && claims?.expiresAt > now
&& claims?.tenant === policy.tenant && Array.isArray(claims?.scopes) && claims.scopes.includes(policy.scope);
}
export function approvedArtifact(bytes, expectedSha256) {
const actual = createHash('sha256').update(bytes).digest();
if (!/^[a-f0-9]{64}$/.test(expectedSha256 ?? '')) return false;
return timingSafeEqual(actual,Buffer.from(expectedSha256,'hex'));
}

Run npm run examples:security and npm run eval:security. This source is a deterministic teaching fixture; it does not prove live model safety, NLI, production auth or OS/network isolation.

Interview questions

What boundary matters in MCP & Connector Security?

The trustedToken fixture evaluates issuer, audience, tenant, scope and expiry only after an adapter has cryptographically verified the token. It must never receive merely decoded attacker-controlled claims as trusted input.

What does the executable fixture demonstrate?

Inspect tool metadata and outputs as untrusted data. Reauthorize calls even when a familiar connector proposes them; a discovered tool is not automatically approved for execution.

What remains a production integration responsibility?

This lesson uses an auth-policy fixture, not a deployed MCP OAuth server or conformance suite. The official security guide and actual selected SDK/transport need separate integration checks.

Practice

Extend one negative scenario and its authorized positive counterexample for MCP & Connector Security.

Check your solution

  • State the trusted input and effect boundary.
  • Assert the expected status and observed effect.
  • Report coverage and limitations without claiming production readiness.

Official references

Continue learning

Category sequence

AI Security

Lesson 11 / 14

Was this lesson useful?

Change notes: 2026-10-31: security/reliability curriculum and source-bound fixture.