# Connected resources Let a WAMP extension expose your service to agents as the current organization and user, without another login or copied API key. Source: https://docs.vampikez.fun/identity/connected-resources/ A connected resource is a normal remote MCP service that accepts the identity the user already selected in WAMP. This page is sufficient to build and deploy the backend, register it, create the customer consent link, and package the Desktop connector without access to the WAMP source repository. The user does not create an account in your service, run another OAuth flow, or copy an API key. You need a WAMP account with `organization.applications.manage` in the organization that will publish the service. If you do not see **Apps** in [Account Center](https://account.vampikez.fun), ask an organization owner to grant that permission or perform the registration steps. ## Production values These values are fixed for the public WAMP deployment. Copy them as written: | Variable | Value | | --- | --- | | `WAMP_ACCOUNT_API` | `https://api.vampikez.fun` | | `WAMP_TOKEN_ISSUER` | `https://api.vampikez.fun` | | `WAMP_JWKS_URL` | `https://api.vampikez.fun/.well-known/jwks.json` | | Account Center | `https://account.vampikez.fun` | For another WAMP deployment, its operator supplies these values. The Account API and token issuer are intentionally distinct from the OIDC issuer ending in `/oauth2`; do not use the OIDC issuer or its JWKS URI for resource identity tokens. ## What is available now | Path | Status | | --- | --- | | Backend authentication and standard Streamable HTTP MCP | Available | | Manifest-only connector imported into WAMP Desktop | Available | | Native and ACP agents in Desktop | Available; the ACP runtime must lend the `mcp` tool category | | Native and ACP agents in managed Cloud | Runtime and identity path available when the connector is included in the Cloud engine image | | User-installed Marketplace connector automatically appearing in Cloud | Not available; arbitrary extensions are not yet delivered into a managed sandbox | | Extension UI reading your HTTP API as the viewer | Available in WAMP Desktop through `auth.resource` and `pluginAPI.auth.resourceFetch`; not yet in managed Cloud or the web workspace | The backend contract does not change across those paths. A backend built from this page is Cloud-compatible; until Cloud extension delivery ships, a WAMP operator must include the same manifest in the Cloud image. Do not put a token in renderer code as a workaround. ## How a request reaches your service 1. An organization authorizes the exact registered resource audience and enables its App for all members or an assigned subset. 2. An installed extension contributes your standard Streamable HTTP MCP URL and registered resource audience. 3. WAMP reauthorizes the current Desktop identity or the Cloud Session's durable execution owner, then mints a short-lived JWT for that exact audience and network origin. 4. The engine sends the JWT as an ordinary bearer on every MCP HTTP request. 5. Your service verifies the JWT locally and applies its own tenant and resource ACL using `org_id` and the pairwise `sub`. 6. Native WAMP agents use the discovered tools directly. ACP agents receive the same tools through WAMP's MCP lending boundary; the JWT and upstream connection never enter the ACP process. There is no WAMP-specific MCP dialect and no WAMP SDK dependency in your service. Authentication is the only added HTTP boundary. A connector does not need an engine or ACP patch; an ACP runtime only needs its existing tool selection to lend the `mcp` category. ## Register the resource Use [Account Center](https://account.vampikez.fun) for the shortest path: 1. Select your publisher organization, open **Apps**, and choose **New app**. Set a stable lowercase slug such as `internal-knowledge`. 2. Open the App, choose **Add key**, then **Generate**. Download the private key when shown; WAMP stores only the public half and cannot recover the private key. Confirm that you saved it to register the public key. 3. Under **Connected resources**, choose **New resource** and enter the name, audience, canonical HTTPS origin, and namespace described below. 4. Copy the **App ID** shown above the resource list. Your backend verifies it as `WAMP_RESOURCE_APP_ID`. No OAuth client or WAMP capability definition is required. The signing key is used only by your backend to create installation consent links; it is never sent to the MCP endpoint or included in the extension. The same registration is available through the public Account API for automation. Its exact request and user-token contract are in [Apps and credentials](/identity/apps-and-credentials/); the Account Center path above requires no token handling. | Value | Contract | | --- | --- | | App slug | The existing WAMP App that owns the service. It does not have to equal the extension directory name. | | Resource name | 1–100 characters, for organization administration. | | Audience | Globally unique, 2–128 characters, matching `^[a-z][a-z0-9-]{1,127}$`. Do not use the platform-reserved `wamp` prefix. | | Origin | The canonical HTTPS origin only: scheme, host, and optional explicit port. No path, trailing slash, query, fragment, or embedded credentials. | | Namespace | Globally unique, at most 96 characters, with at least two lowercase dot-separated segments, such as `internal.docs`. It reserves future capability names even when this integration defines none. | Configure the deployed service with the values from Account Center and the production values at the top of this page: ```bash title="service.env" WAMP_TOKEN_ISSUER=https://api.vampikez.fun WAMP_JWKS_URL=https://api.vampikez.fun/.well-known/jwks.json WAMP_RESOURCE_AUDIENCE=internal-docs WAMP_RESOURCE_ORIGIN=https://knowledge.example.com WAMP_RESOURCE_APP_ID=YOUR_APP_ID HOST=0.0.0.0 PORT=3000 ``` The audience, origin, namespace, and App id are security identifiers, not display configuration. Once the origin is bound, it cannot be retargeted; use a new audience for a different production origin. Give staging its own audience and HTTPS origin as well. Although the MCP URL may include a path such as `/mcp`, its `new URL(url).origin` must equal the registered origin exactly. List active bindings with `GET /api/apps/:slug/resource-servers`. Retire one with `DELETE /api/apps/:slug/resource-servers/:resourceServerId`. Retirement is global and irreversible: WAMP stops issuing new tokens, existing tokens drain within 15 minutes, and the audience and namespace remain reserved so they cannot silently acquire a new meaning. ## Let an organization install the resource The service backend creates a normal App installation intent with an empty capability list. Empty means **identity only**, and the Account API accepts it only when the requested audience belongs to the App that signed the assertion. The following Node 22 script creates an intent, or polls one when given its id. Copy the signing key `kid` from the App's key list in Account Center: ```bash npm install jose@6.2.3 ``` ```js title="installation-intent.mjs" const required = [ 'WAMP_ACCOUNT_API', 'WAMP_TOKEN_ISSUER', 'WAMP_APP_SLUG', 'WAMP_APP_KID', 'WAMP_APP_PRIVATE_KEY_FILE', 'WAMP_RESOURCE_AUDIENCE', ]; for (const name of required) { if (!process.env[name]) throw new Error(`Missing ${name}`); } const key = await importPKCS8( await readFile(process.env.WAMP_APP_PRIVATE_KEY_FILE, 'utf8'), 'EdDSA', ); async function createAssertion() { const now = Math.floor(Date.now() / 1000); return new SignJWT({}) .setProtectedHeader({ alg: 'EdDSA', kid: process.env.WAMP_APP_KID }) .setIssuer(process.env.WAMP_APP_SLUG) .setSubject(process.env.WAMP_APP_SLUG) .setAudience(process.env.WAMP_TOKEN_ISSUER) .setIssuedAt(now) .setExpirationTime(now + 300) .sign(key); } async function post(path, body) { const response = await fetch(`${process.env.WAMP_ACCOUNT_API}${path}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), }); const result = await response.json(); if (!response.ok) throw new Error(`${response.status}: ${JSON.stringify(result)}`); return result.intent; } const intentId = process.argv[2]; const assertion = await createAssertion(); const intent = intentId ? await post(`/api/apps/installation-intents/${encodeURIComponent(intentId)}/status`, { assertion, }) : await post('/api/apps/installation-intents', { assertion, resourceAudience: process.env.WAMP_RESOURCE_AUDIENCE, capabilityIds: [], }); console.log(intent); ``` Replace `YOUR_KEY_ID`, set the remaining values, and create the link: ```bash export WAMP_ACCOUNT_API=https://api.vampikez.fun export WAMP_TOKEN_ISSUER=https://api.vampikez.fun export WAMP_APP_SLUG=internal-knowledge export WAMP_APP_KID=YOUR_KEY_ID export WAMP_APP_PRIVATE_KEY_FILE=./internal-knowledge.private.pem export WAMP_RESOURCE_AUDIENCE=internal-docs node installation-intent.mjs ``` Send the returned `authorizeUrl` to the administrator, then poll with `node installation-intent.mjs `. The administrator signs into WAMP if needed, chooses an organization, and approves disclosure of its organization identity and one pairwise service-specific user identity. Store the returned `installationId` when status becomes `authorized`. This is installation consent, not OAuth: the customer creates no account in your service and copies no key. The exact App assertion contract is in [Apps and credentials](/identity/apps-and-credentials/#the-assertion). The status is `pending`, `authorized`, or `expired`. Stop polling on either terminal status; create a new intent after `expired`. Approval is durable for this exact audience and installation. Registering a second resource on the same App does not inherit an older installation or another resource's approval: create and authorize another identity-only intent. Revoking the App installation revokes every resource approval attached to it. Installing the connector extension does not silently install the App. Keep the explicit consent link: the extension controls whether the connector exists on a host, while the Account installation controls which organization and members may disclose identity to the service. An organization administrator can later narrow or revoke the installation through the standard [product access controls](/identity/organizations/#product-access). No token is issued when the organization, App, installation, membership, or assigned-member access is inactive. ## Package the Desktop connector The connector is a manifest-only WAMP extension. It has no JavaScript, `package.json`, SDK dependency, build step, or stored credential. Create a directory named `internal-knowledge-connector` containing one file: ```json title="extension.json" { "name": "Internal Knowledge", "version": "1.0.0", "contributes": { "mcpServers": [ { "id": "knowledge", "transport": "http", "url": "https://knowledge.example.com/mcp", "wampAuth": { "audience": "internal-docs" } } ] } } ``` The directory name is the extension id. To hand the connector to a Desktop user, zip the directory itself: ```bash zip -r internal-knowledge-connector.zip internal-knowledge-connector ``` In WAMP Desktop, open **Marketplace**, choose **Add → Import package…**, select the zip, and enable the extension. The WAMP operator may curate the same manifest-only package into the public catalog. Installing an extension and approving its App installation are separate actions: both must be present before WAMP can mint identity for the MCP connection. `wampAuth` is valid only for `http` or `sse` at an HTTPS URL without embedded credentials. It cannot be combined with `oauth` or a static `Authorization` header. Other static headers are allowed, but they are not identity. The bearer is obtained and attached by the host; it never enters `extension.json`, extension storage, renderer JavaScript, or agent context. Managed identity may come only from the host-owned global MCP layer, normally through an installed extension. A repository-owned `.wamp/mcp-servers.json` is allowed to declare ordinary MCP servers but cannot set `wampAuth`. This prevents a checked out repository from silently requesting the signed-in user's identity for an arbitrary service. `wampAuth` authenticates the host-managed MCP transport only. Extension UI that reads your service directly declares `auth.resource` instead; see [Read your service from extension UI](#read-your-service-from-extension-ui). Never copy a token into renderer code. The backend developer hands the extension publisher only two security-relevant strings: the full MCP endpoint URL and the registered audience. The App id, JWKS URL, token issuer, and App private key stay in the backend's configuration; none belongs in `extension.json`. Removing or disabling the extension removes the MCP contribution and its token demand. Your service is therefore optional: a failed or absent connector does not stop WAMP or the agent runtime. ## Read your service from extension UI An extension panel can call your HTTP API as the person viewing it. WAMP Desktop holds the resource identity JWT and sends the request; the panel receives the response, never the token. Declare the audience and the API base in `extension.json`: ```json { "compat": { "pluginApi": "^4.0.0" }, "requiresElectron": true, "permissions": [ { "auth.resource": [ { "audience": "internal-docs", "baseUrl": "https://docs.example.com/api/v1" } ] } ] } ``` `baseUrl` is a canonical HTTPS URL with a path and no credentials, query, fragment or trailing slash. The audience is the one registered for your resource. An extension may declare up to eight audiences, each once. Keep `requiresElectron` until Cloud hosts support these reads. From the extension UI: ```ts const response = await pluginAPI.auth.resourceFetch( { audience: 'internal-docs', path: '/api/v1/documents?limit=20' }, { signal }, ); // { status, headers: { 'content-type'?, 'retry-after'?, 'x-request-id'? }, body } ``` `path` is origin-relative, includes the base path and must stay under it. WAMP refuses a path containing a fragment, whitespace, a control character or a backslash; a pathname with a `.` or `..` segment or an encoded `/`, `\`, `.` or `%`; and a query that does not round-trip through `URLSearchParams`. It sends exactly the declared origin followed by `path`. Your backend receives one `GET` with `Accept: application/json` and `Authorization: Bearer `, the same token described in [What WAMP sends](#what-wamp-sends). No cookie, `Origin` or extension-chosen header is sent. | Rule | Behavior | | --- | --- | | Methods | `GET` only | | Redirects | Not followed; any `3xx` fails the call | | Timeout | 20 seconds | | Response size | 256 KiB after decompression | | Response type | `application/json` or a `+json` type; anything else fails the call | | `401` | WAMP exchanges a fresh identity once and retries once; a second `401` is returned | HTTP statuses, including `403`, `404`, `429` and `5xx`, resolve as responses with their parsed JSON body. A call without a trustworthy response rejects with a `ResourceFetchError` whose `code` is: | `code` | Meaning | | --- | --- | | `not_declared` | The audience is not in this extension's `auth.resource` | | `invalid_path` | The path breaks the rules above | | `signed_out` | Nobody is signed in to WAMP | | `access_denied` | Account refused the identity; `reason` is `app_not_installed`, `member_not_assigned` or `resource_not_registered` when Account names one | | `unavailable` | Account or your service could not be reached; retry, after `retryAfter` seconds when present | | `redirect_refused`, `response_too_large`, `invalid_response`, `timeout` | The response broke a rule in the table above | | `identity_changed` | The viewer signed out or changed account while the request was out; start over | | `host_unsupported` | This host has no renderer resource reads (currently Cloud and the web workspace) | ## What WAMP sends Every MCP HTTP request carries: ```http Authorization: Bearer ``` The JWT is Ed25519-signed, has `typ: at+jwt`, and is short-lived: 15 minutes by default. Its audience is one exact string. | Claim | Meaning | | --- | --- | | `iss` | The exact WAMP platform token issuer supplied during registration. | | `aud` | The registered resource audience, for example `internal-docs`. | | `sub` | A pairwise subject. A human subject is stable for this resource App across Desktop and Cloud. An App-installation subject is stable while that source installation exists and changes after it is reinstalled. | | `org_id` | The WAMP organization whose installation authorized disclosure. | | `subject_kind` | `human` identifies the execution owner. `app_installation` identifies a non-human App installation executing a Cloud workload; it is not a WAMP user. | | `resource_app_id` | The immutable App that owns the resource server. | | `resource_installation_id` | That App's current installation in `org_id`. This changes after revocation and reinstallation. | | `resource_origin` | The registered origin allowed to receive the token. | | `token_use` | Exactly `resource_identity`. | | `iat`, `nbf`, `exp`, `jti` | Issuance window and a unique token id. `jti` is useful for audit correlation; it is not a single-use nonce. | There is deliberately no email, name, global WAMP user id, membership id, parent grant, service permission, or service capability in this token. Store a local principal under the compound key `(iss, org_id, sub)`. Do not use `resource_installation_id` as the user key. Your service may create the local principal lazily on the first accepted request, so the user never needs a second registration flow. Persist `subject_kind` with that principal. Apply member/user ACLs only to `human`; handle `app_installation` as a service principal with its own ACL, or deny it if your service does not support non-human callers. ## Build a working MCP backend The following Node 22 service is a complete authenticated Streamable HTTP MCP endpoint with one diagnostic tool. It verifies identity before parsing the MCP body, keeps no WAMP credential, and creates a stateless MCP transport per request. Start with these two files: ```json title="package.json" { "name": "internal-knowledge-mcp", "private": true, "type": "module", "scripts": { "start": "node server.mjs" }, "dependencies": { "@modelcontextprotocol/sdk": "1.30.0", "express": "5.2.1", "jose": "6.2.3", "zod": "3.25.76" } } ``` ```js title="resource-auth.mjs" const ISSUER = process.env.WAMP_TOKEN_ISSUER; const JWKS_URL = process.env.WAMP_JWKS_URL; const AUDIENCE = process.env.WAMP_RESOURCE_AUDIENCE; const ORIGIN = process.env.WAMP_RESOURCE_ORIGIN; const APP_ID = process.env.WAMP_RESOURCE_APP_ID; const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; if (!ISSUER || !JWKS_URL || !AUDIENCE || !ORIGIN || !APP_ID) { throw new Error('WAMP resource identity configuration is incomplete'); } const jwks = createRemoteJWKSet(new URL(JWKS_URL)); export class ResourceIdentityAuthorityUnavailableError extends Error {} function isAuthorityUnavailable(error) { return error instanceof TypeError || error instanceof errors.JWKSTimeout || error instanceof errors.JWKSInvalid || error instanceof errors.JWKInvalid || error?.constructor === errors.JOSEError; } export async function verifyWampResourceBearer(authorization) { const match = /^Bearer ([^\s]+)$/i.exec(authorization ?? ''); if (!match) throw new Error('missing WAMP resource bearer'); let payload; try { ({ payload } = await jwtVerify(match[1], jwks, { issuer: ISSUER, audience: AUDIENCE, algorithms: ['EdDSA'], typ: 'at+jwt', clockTolerance: 30, requiredClaims: [ 'iss', 'aud', 'sub', 'exp', 'iat', 'nbf', 'jti', 'org_id', 'subject_kind', 'resource_app_id', 'resource_installation_id', 'resource_origin', 'token_use', ], })); } catch (error) { if (isAuthorityUnavailable(error)) { throw new ResourceIdentityAuthorityUnavailableError( 'WAMP resource identity authority is unavailable', { cause: error }, ); } throw error; } if (payload.aud !== AUDIENCE || payload.token_use !== 'resource_identity' || payload.resource_origin !== ORIGIN || payload.resource_app_id !== APP_ID || (payload.subject_kind !== 'human' && payload.subject_kind !== 'app_installation') || typeof payload.sub !== 'string' || payload.sub.length === 0 || typeof payload.org_id !== 'string' || !UUID.test(payload.org_id) || typeof payload.resource_installation_id !== 'string' || !UUID.test(payload.resource_installation_id)) { throw new Error('invalid WAMP resource identity'); } return { issuer: payload.iss, organizationId: payload.org_id, subject: payload.sub, subjectKind: payload.subject_kind, installationId: payload.resource_installation_id, tokenId: payload.jti, expiresAt: payload.exp, }; } ``` ```js title="server.mjs" ResourceIdentityAuthorityUnavailableError, verifyWampResourceBearer, } from './resource-auth.mjs'; const host = process.env.HOST ?? '127.0.0.1'; const port = Number(process.env.PORT ?? 3000); const app = express(); app.get('/healthz', (_request, response) => { response.json({ ok: true }); }); async function authenticate(request, response, next) { try { request.wampPrincipal = await verifyWampResourceBearer( request.get('authorization'), ); next(); } catch (error) { if (error instanceof ResourceIdentityAuthorityUnavailableError) { response .set('Retry-After', '5') .status(503) .json({ error: 'identity_authority_unavailable' }); return; } response .set('WWW-Authenticate', 'Bearer error="invalid_token"') .status(401) .json({ error: 'invalid_token' }); } } function createServer(principal) { const server = new McpServer({ name: 'internal-knowledge', version: '1.0.0', }); server.registerTool( 'wamp_identity', { description: 'Return the WAMP organization and pairwise service identity.', inputSchema: {}, }, async () => ({ content: [{ type: 'text', text: JSON.stringify({ organizationId: principal.organizationId, subject: principal.subject, subjectKind: principal.subjectKind, }), }], }), ); return server; } app.post('/mcp', authenticate, express.json({ limit: '1mb' }), async (request, response) => { const server = createServer(request.wampPrincipal); const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); response.on('close', () => { void transport.close(); void server.close(); }); try { await server.connect(transport); await transport.handleRequest(request, response, request.body); } catch (error) { console.error('MCP request failed', error); if (!response.headersSent) { response.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error' }, id: null, }); } } }); app.all('/mcp', authenticate, (_request, response) => { response.status(405).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Method not allowed' }, id: null, }); }); app.listen(port, host, () => { console.log(`MCP server listening on http://${host}:${port}/mcp`); }); ``` Replace `YOUR_APP_ID`, run `npm install`, then start locally with `node --env-file=service.env server.mjs`. In production, load the same values through your deployment's secret/config mechanism. Deploy the final `/mcp` endpoint at the exact registered HTTPS origin without a redirect. Replace the diagnostic tool with your service tools after the identity smoke test. Every data query must scope its own authorization by `principal.organizationId` and, when user-level access matters, `principal.subject`. The JWKS URL normally ends in `/.well-known/jwks.json`. It is public and cacheable; `createRemoteJWKSet` caches keys and refreshes on a new `kid`, so do not fetch it separately on every request. Keep the issuer, audience, algorithm, type, origin, App id, and semantic claim checks even though the signature is valid. Accepting a correctly signed JWT for another WAMP service is still an authorization bug. Verify the bearer before parsing or executing the MCP request. Then apply your own ACL: | Result | HTTP response | | --- | --- | | Missing, malformed, expired, or semantically invalid bearer | `401` and, optionally, `WWW-Authenticate: Bearer error="invalid_token"` | | WAMP JWKS cannot be fetched or parsed and no usable cached key exists | `503`; never mislabel an authority outage as `invalid_token` | | Valid identity without access to the requested document or operation | `403` | | Valid identity and allowed operation | Continue into the normal MCP handler | Return real HTTP `401`/`403` responses rather than a successful JSON-RPC response containing an authentication error. WAMP uses the HTTP status to retire a rejected managed connection. It does not launch MCP OAuth for a `wampAuth` connector. The user can open **Settings → MCP** and click **Retry** to request a fresh managed token; sign-in, organization switch, and WAMP credential rotation also rebuild the connection. A persistent `401` remains a connection error until the service accepts the fresh token or the organization restores its approval. Verify the bearer on every HTTP request. For SSE, Streamable HTTP GET, or any other long-lived response, close the response and its server-side MCP session no later than the verified `exp`. WAMP also rotates its client transport before expiry, but your service must enforce the JWT boundary itself. Offline verification means installation or membership revocation is bounded by the token's remaining lifetime. Rotation retires the managed transport before expiry, so a tool call still in flight at that boundary can end with a transport error. Make mutating tools idempotent and safe to retry; WAMP does not silently replay an ambiguous call. Do not redirect an authenticated MCP request. WAMP refuses every `3xx` on this path so a bearer cannot cross origins, and it does not replay an ambiguous mutating MCP request. Serve the final MCP endpoint at the URL in the manifest. ## Runtime behavior after the connector is present | Runtime | Identity and network path | | --- | --- | | Desktop | WAMP silently exchanges the current Desktop OIDC grant. The local engine calls your MCP URL from the user's machine. Login, logout, organization switch, or credential rotation retires the old identity-bound connection. | | Cloud native agent | When the connector manifest is included in the Cloud engine image, the control plane reauthorizes the Session's durable execution owner and installs the JWT in the sandbox engine's memory. The sandbox or organization runner calls your MCP URL directly; the Account service is not a traffic proxy. | | Cloud ACP agent | With that same image requirement, the sandbox engine connects to your service, discovers the tool, and lends a credential-free WAMP MCP facade to the ACP process. Tool calls return through that facade. The ACP agent never receives your URL credential or direct upstream connection. | Installing a connector from Desktop Marketplace does not currently copy it into managed Cloud sandboxes. A WAMP operator must add its unchanged manifest to the Cloud engine image. After that delivery step, your DNS, TLS, firewall, and routing must work from the selected sandbox runner. A private service reachable only on an organization's network can use that organization's self-hosted runner; managed sandboxes need a route to the service. Network source addresses are not identity — always authorize the JWT. Cloud keeps the Session execution owner as the connected-resource subject. Sharing a Session intentionally shares that sandbox's capabilities; unrelated Sessions and organizations do not reuse the same lease. Design agent behavior so an optional or temporarily unavailable tool may be absent. A Cloud workspace may require at most eight distinct managed `(audience, origin)` pairs. On the first tool snapshot for a new configuration or credential set, WAMP waits up to five seconds for their MCP discovery; a slower or failed optional server is absent from that snapshot and continues connecting in the background. ## Test the integration 1. Register a staging audience and HTTPS origin in Account Center. Create an identity-only installation link and approve it for a test organization. Keep `ALL_MEMBERS` for the first smoke test. 2. Deploy the example MCP service at the exact manifest URL and configure the five service values listed above. Confirm `/healthz` answers before involving WAMP. 3. Import the connector zip through **Marketplace → Add → Import package…**, sign into WAMP Desktop, select the test organization, and enable the extension. You do not extract or paste a bearer. 4. Ask a native agent to call `wamp_identity`. Confirm the service sees the expected `iss`, `org_id`, pairwise `sub`, `resource_app_id`, and `resource_origin`. 5. Run the same call through a Desktop ACP runtime whose manifest lends the `mcp` tool category. Confirm the service sees the same principal and no ACP credential. 6. If a WAMP operator has included the connector in the Cloud image, run the same native and ACP calls from a Cloud Session. This is not a self-service extension installation step today. 7. Change the installation to `ASSIGNED_MEMBERS`, exclude the test member, and confirm a fresh Desktop login or Cloud Session cannot obtain the tool. A bearer already issued to an existing Session remains valid only until its `exp`. Log claim identifiers and authorization decisions, never the bearer. Treat the pairwise `sub` as a pseudonymous user identifier and protect it like other account data.