test(connections): mirror a real MCP-direct vendor's advertised shape

PAP-18519, an acceptance test for the connector skills shipped on
PAP-18464. Michael asked for an Enterpret connector as the test case.
There is no authorized Enterpret credential and Paperclip's connect flow
performs RFC 7591 dynamic client registration automatically, so pointing
a connect flow at the live endpoint would create a client record at a
vendor we have no relationship with. Instead this mirrors the vendor's
public, unauthenticated metadata into the existing in-process fixture
and exercises the real broker against it.

The documents are literal copies of what
https://wisdom-api.enterpret.com/server/mcp and
https://oauth.enterpret.com published on 23 September 2026, re-hosted on
the DNS-pinned IP literals this file already uses. Nothing here reaches
the vendor: no registration, no consent, no tool call.

Four things the existing fixture does not cover, each on a different
rung of the client-resolution ladder:

- The 401 challenge carries realm and scope around resource_metadata,
  not that parameter alone.
- The authorization server is on a different origin from the MCP server
  and its issuer has no path, so RFC 8414 insertion never applies.
- The authorization server advertises no Client ID Metadata Document, so
  DCR is the only rung whatever the callback origin looks like -- the
  playbook frames CIMD as gated on a public HTTPS origin, and for this
  shape that framing is not the binding constraint.
- It advertises no revocation_endpoint, and Paperclip never looks for
  one: revoke is enforced entirely locally.

Recorded adverse finding, asserted rather than described: with no
annotations to go on, name inference files all ten documented tools as
read-only, including run_graph_query and execute_cypher_query, which
execute operator-supplied queries against the vendor's customer-feedback
graph. The wizard shows ten reads and no changes.

server 70/70 in this file, server typecheck clean.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
nguyenm7andPaperclip committed 2026-09-23 23:39:39 +00:00
1 parent 02fc853af2
commit 1aeba0e6c2
1 file changed
+477
@@ -276,6 +276,191 @@ function installMcpOAuthFixture(options: FixtureOptions = {}) {
};
}
/**
* PAP-18519 — the same MCP endpoint, re-shaped to match a real MCP-direct
* vendor field for field.
*
* Every document below is a literal copy of a **public, unauthenticated**
* metadata response read from `https://wisdom-api.enterpret.com/server/mcp` and
* `https://oauth.enterpret.com` on 23 September 2026, re-hosted on the
* DNS-pinned IP literals this file already uses. Nothing in this fixture talks
* to that vendor: no client was registered against its authorization server, no
* consent was given, and no tool was called. The tool list is the vendor's
* *documented* list — an authenticated `tools/list` has never been observed —
* so treat discovered names here as a shape, not as proof of the live catalog.
*
* It is a second fixture rather than an option on the one above because the
* shape differs in four ways that each land on a different rung of the
* client-resolution ladder:
*
* 1. the 401 challenge carries `realm` and `scope` *around* the
* `resource_metadata` parameter, rather than that parameter alone;
* 2. the authorization server is on a different origin from the MCP server
* and its issuer has no path, so RFC 8414 well-known insertion never
* applies and only the origin form can answer;
* 3. it advertises no Client ID Metadata Document, so CIMD is unavailable
* whatever the callback origin looks like; and
* 4. it advertises no `revocation_endpoint`.
*/
const VENDOR_SHAPED_MCP_URL = `${MCP_ORIGIN}/server/mcp`;
/** A pathless issuer on its own origin, the way a vendor splits API from auth. */
const VENDOR_SHAPED_ISSUER = "https://1.1.1.1";
/** The URL the vendor's own challenge names: suffix form, under the resource path. */
const VENDOR_SHAPED_RESOURCE_METADATA_URL = `${VENDOR_SHAPED_MCP_URL}/.well-known/oauth-protected-resource`;
/** The insertion form (RFC 9728 §3.1) the vendor *also* serves, and Paperclip probes. */
const VENDOR_SHAPED_RESOURCE_METADATA_INSERTION_URL = `${MCP_ORIGIN}/.well-known/oauth-protected-resource/server/mcp`;
const VENDOR_SHAPED_CHALLENGE =
`Bearer realm="mcp", resource_metadata="${VENDOR_SHAPED_RESOURCE_METADATA_URL}", scope="mcp:read mcp:write"`;
/**
* The vendor's documented tools, in its documented order, with the three legacy
* aliases it still serves. No `annotations` are supplied because none have been
* observed — so these exercise Paperclip's name-only risk inference, which is
* exactly what an operator gets from a pasted URL.
*/
const VENDOR_SHAPED_TOOLS = [
{ name: "get_organization_details", description: "Organization details" },
{ name: "get_graph_schema", description: "Feedback graph schema" },
{ name: "get_query_examples", description: "Example graph queries" },
{ name: "search_graph_fields", description: "Search graph fields" },
{ name: "search_graph_values", description: "Search graph values" },
{ name: "run_graph_query", description: "Run a graph query" },
{ name: "find_user_quote", description: "Find a customer quote" },
{ name: "get_schema", description: "Legacy alias for get_graph_schema" },
{ name: "execute_cypher_query", description: "Legacy alias for run_graph_query" },
{ name: "search_knowledge_graph", description: "Legacy alias for search_graph_values" },
];
/**
* Authorization-server metadata copied verbatim from the vendor's RFC 8414
* document. `revocation_endpoint` is absent in the original and must stay
* absent here — scenario 8 of the runbook turns on it.
*/
const VENDOR_SHAPED_AUTHORIZATION_SERVER_METADATA = {
authorization_endpoint: `${VENDOR_SHAPED_ISSUER}/authorize`,
code_challenge_methods_supported: ["S256"],
grant_types_supported: ["authorization_code", "refresh_token"],
introspection_endpoint: `${VENDOR_SHAPED_ISSUER}/introspect`,
issuer: VENDOR_SHAPED_ISSUER,
jwks_uri: `${VENDOR_SHAPED_ISSUER}/jwks.json`,
registration_endpoint: `${VENDOR_SHAPED_ISSUER}/register`,
response_types_supported: ["code"],
scopes_supported: ["email", "mcp:read", "mcp:write"],
token_endpoint: `${VENDOR_SHAPED_ISSUER}/token`,
token_endpoint_auth_methods_supported: ["none"],
userinfo_endpoint: `${VENDOR_SHAPED_ISSUER}/userinfo`,
} as const;
/** Protected-resource metadata, verbatim. Note `email` alongside the mcp scopes. */
const VENDOR_SHAPED_PROTECTED_RESOURCE_METADATA = {
resource: VENDOR_SHAPED_MCP_URL,
authorization_servers: [VENDOR_SHAPED_ISSUER],
scopes_supported: ["email", "mcp:read", "mcp:write"],
} as const;
function installVendorShapedFixture() {
const requests: FixtureRequest[] = [];
const issuedCodes = new Set<string>();
let accessToken: string | null = null;
const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(async (url, init) => {
const href = String(url);
const method = (init?.method ?? "GET").toUpperCase();
const headers = headerRecord(init);
const bodyText = typeof init?.body === "string" ? init.body : init?.body?.toString?.() ?? null;
const parsedBody = bodyText
? headers["content-type"]?.includes("json")
? (JSON.parse(bodyText) as Record<string, unknown>)
: new URLSearchParams(bodyText)
: null;
requests.push({ method, url: href, headers, body: parsedBody });
if (href === VENDOR_SHAPED_MCP_URL) {
if (headers.authorization !== `Bearer ${accessToken}`) {
// The vendor's own challenge, byte for byte: realm first, the resource
// metadata URL second, an advertised scope last.
return {
ok: false,
status: 401,
headers: {
get: (name: string) =>
name.toLowerCase() === "www-authenticate" ? VENDOR_SHAPED_CHALLENGE : null,
},
text: async () => "",
json: async () => ({}),
} as unknown as Response;
}
return jsonResponse({
jsonrpc: "2.0",
id: "paperclip-catalog-refresh",
result: { tools: VENDOR_SHAPED_TOOLS },
});
}
// The vendor answers both RFC 9728 forms with the same document.
if (
href === VENDOR_SHAPED_RESOURCE_METADATA_URL ||
href === VENDOR_SHAPED_RESOURCE_METADATA_INSERTION_URL
) {
return jsonResponse(VENDOR_SHAPED_PROTECTED_RESOURCE_METADATA);
}
// A pathless issuer has exactly one well-known location per suffix, and the
// vendor serves both suffixes with the same document.
if (
href === `${VENDOR_SHAPED_ISSUER}/.well-known/oauth-authorization-server` ||
href === `${VENDOR_SHAPED_ISSUER}/.well-known/openid-configuration`
) {
return jsonResponse(VENDOR_SHAPED_AUTHORIZATION_SERVER_METADATA);
}
if (href === `${VENDOR_SHAPED_ISSUER}/register` && method === "POST") {
const requested = parsedBody as Record<string, unknown>;
return jsonResponse({
client_id: "vendor-shaped-dcr-client",
redirect_uris: requested.redirect_uris,
grant_types: requested.grant_types,
response_types: requested.response_types,
token_endpoint_auth_method: requested.token_endpoint_auth_method,
});
}
if (href === `${VENDOR_SHAPED_ISSUER}/token` && method === "POST") {
const body = parsedBody as URLSearchParams;
if (body.get("grant_type") === "authorization_code" && !issuedCodes.has(body.get("code") ?? "")) {
return jsonResponse({ error: "invalid_grant" }, 400);
}
accessToken = `vendor-shaped-access-${randomUUID()}`;
return jsonResponse({
access_token: accessToken,
refresh_token: "vendor-shaped-refresh",
expires_in: 3600,
token_type: "Bearer",
scope: "mcp:read mcp:write",
});
}
return jsonResponse({ error: "not_found" }, 404);
});
return {
fetchMock,
requests,
issueAuthorizationCode(authorizationUrl: string) {
void new URL(authorizationUrl);
const code = `vendor-shaped-code-${randomUUID()}`;
issuedCodes.add(code);
return code;
},
requestsMatching(pattern: RegExp) {
return requests.filter((entry) => pattern.test(entry.url));
},
urlsFetched() {
return requests.map((entry) => `${entry.method} ${entry.url}`);
},
};
}
async function createCompany(db: ReturnType<typeof createDb>) {
const company = await db
.insert(companies)
@@ -2469,4 +2654,296 @@ describeEmbeddedPostgres("generic remote MCP connections", () => {
expect(response.body.redirect_uris).toEqual([REDIRECT_URI]);
});
/**
* PAP-18519 — an acceptance test against a mirror of a real MCP-direct
* vendor's advertised metadata, standing in for the vendor itself because
* there is no authorized account and pointing a connect flow at the live
* endpoint would dynamically register a client we have no permission to
* create. See `installVendorShapedFixture` for what the mirror copies and
* where it was read from.
*/
describe("a vendor-shaped MCP-direct provider (PAP-18519)", () => {
it("resolves the whole endpoint set from the 401 challenge alone", async () => {
const fixture = installVendorShapedFixture();
const company = await createCompany(db);
const service = toolAccessService(db);
// The operator supplies a URL and a name. No manifest, no endpoint pair,
// no issuer — exactly what a catalog definition that ships `serverUrl`
// only would hand the broker.
const connected = await service.connectGalleryApp(company.id, {
link: VENDOR_SHAPED_MCP_URL,
name: "Vendor shaped",
});
expect(connected.auth).toMatchObject({
kind: "oauth",
issuer: VENDOR_SHAPED_ISSUER,
resource: VENDOR_SHAPED_MCP_URL,
});
const [connection] = await db
.select()
.from(toolConnections)
.where(eq(toolConnections.id, connected.connectionId));
expect(connection!.config.oauth).toMatchObject({
authorizationUrl: `${VENDOR_SHAPED_ISSUER}/authorize`,
tokenUrl: `${VENDOR_SHAPED_ISSUER}/token`,
registrationUrl: `${VENDOR_SHAPED_ISSUER}/register`,
metadataUrl: VENDOR_SHAPED_RESOURCE_METADATA_URL,
issuer: VENDOR_SHAPED_ISSUER,
resource: VENDOR_SHAPED_MCP_URL,
codeChallengeMethodsSupported: ["S256"],
tokenEndpointAuthMethodsSupported: ["none"],
clientIdMetadataDocumentSupported: false,
});
// The challenge's own `scope` wins over the protected-resource document's
// `scopes_supported`, so the `email` scope the resource advertises is
// never requested. That narrowing is the provider's, not a reviewed
// Paperclip allowlist — a catalog entry is where a reviewed one would go.
expect(connection!.config.oauth).toMatchObject({ scopes: ["mcp:read", "mcp:write"] });
// Discovery followed the challenge to the document the vendor named, and
// from there to the pathless issuer's only well-known location. The
// insertion form the vendor also serves was never needed.
const discovery = fixture.urlsFetched();
expect(discovery).toEqual([
`POST ${VENDOR_SHAPED_MCP_URL}`,
`GET ${VENDOR_SHAPED_RESOURCE_METADATA_URL}`,
`GET ${VENDOR_SHAPED_ISSUER}/.well-known/oauth-authorization-server`,
]);
expect(fixture.requestsMatching(/oauth-protected-resource\/server\/mcp$/)).toHaveLength(0);
});
it("falls through to dynamic registration whether or not the callback is public HTTPS", async () => {
const fixture = installVendorShapedFixture();
const company = await createCompany(db);
const service = toolAccessService(db);
const connected = await service.connectGalleryApp(company.id, {
link: VENDOR_SHAPED_MCP_URL,
name: "Vendor shaped ladder",
});
// A self-hosted instance with no public origin at all: the callback is
// loopback, so no authorization server could fetch a client metadata
// document even if one were advertised.
const loopbackStart = await service.startOAuth(company.id, connected.connectionId, {
redirectUri: "http://localhost:3100/api/tools/oauth/callback",
actor: { actorType: "user", actorId: "board-user" },
});
expect(loopbackStart.registrationSource).toBe("dcr");
const registration = fixture.requestsMatching(/\/register$/);
expect(registration).toHaveLength(1);
expect(registration[0]!.body).toMatchObject({
redirect_uris: ["http://localhost:3100/api/tools/oauth/callback"],
token_endpoint_auth_method: "none",
grant_types: ["authorization_code", "refresh_token"],
response_types: ["code"],
});
const authorizationUrl = new URL(loopbackStart.authorizationUrl);
expect(authorizationUrl.origin + authorizationUrl.pathname).toBe(`${VENDOR_SHAPED_ISSUER}/authorize`);
expect(authorizationUrl.searchParams.get("client_id")).toBe("vendor-shaped-dcr-client");
expect(authorizationUrl.searchParams.get("code_challenge_method")).toBe("S256");
expect(authorizationUrl.searchParams.get("code_challenge")).toBeTruthy();
expect(authorizationUrl.searchParams.get("resource")).toBe(VENDOR_SHAPED_MCP_URL);
expect(authorizationUrl.searchParams.get("scope")).toBe("mcp:read mcp:write");
// The interesting half: a *public HTTPS* callback does not change the
// answer, because this authorization server never advertises CIMD. The
// playbook frames CIMD as gated on a public origin; for this vendor the
// rung is missing on the server side, so DCR is the only rung there is.
const publicCompany = await createCompany(db);
const publicService = toolAccessService(db, {
oauthClientMetadataLookup: async () => [{ address: "93.184.216.34", family: 4 }],
});
const publicConnected = await publicService.connectGalleryApp(publicCompany.id, {
link: VENDOR_SHAPED_MCP_URL,
name: "Vendor shaped public",
});
const publicStart = await publicService.startOAuth(publicCompany.id, publicConnected.connectionId, {
redirectUri: REDIRECT_URI,
actor: { actorType: "user", actorId: "board-user" },
});
expect(publicStart.registrationSource).toBe("dcr");
expect(new URL(publicStart.authorizationUrl).searchParams.get("client_id"))
.not.toBe(CLIENT_METADATA_DOCUMENT_URL);
});
it("completes the flow and enforces revoke locally, with no revocation endpoint to call", async () => {
const fixture = installVendorShapedFixture();
const company = await createCompany(db);
const service = toolAccessService(db);
const policy = toolAccessPolicyService(db);
const [agent] = await db
.insert(agents)
.values({
companyId: company.id,
name: `Vendor shaped agent ${randomUUID()}`,
role: "engineer",
status: "active",
adapterType: "process",
adapterConfig: {},
runtimeConfig: {},
})
.returning();
const connected = await service.connectGalleryApp(company.id, {
link: VENDOR_SHAPED_MCP_URL,
name: "Vendor shaped revoke",
});
const start = await service.startOAuth(company.id, connected.connectionId, {
redirectUri: REDIRECT_URI,
actor: { actorType: "user", actorId: "board-user" },
});
const state = new URL(start.authorizationUrl).searchParams.get("state")!;
const completed = await service.completeOAuthCallback({
state,
code: fixture.issueAuthorizationCode(start.authorizationUrl),
iss: VENDOR_SHAPED_ISSUER,
redirectUri: REDIRECT_URI,
actor: { actorType: "user", actorId: "board-user" },
});
// The whole documented tool surface arrives through the generic path,
// with risk inferred from names alone because the server annotates
// nothing. `run_graph_query` and `execute_cypher_query` read as writes:
// correct to be cautious, and a reason a curated entry would be clearer.
const discovered = [
...completed.actions.readOnly.map((action) => action.toolName),
...completed.actions.canMakeChanges.map((action) => action.toolName),
].sort();
expect(discovered).toEqual(VENDOR_SHAPED_TOOLS.map((tool) => tool.name).sort());
// Adverse, and the strongest single argument for a curated entry: with no
// `annotations` to go on, name inference puts *every* tool in the
// read-only column — including `run_graph_query` and its legacy alias
// `execute_cypher_query`, which execute operator-supplied queries against
// the vendor's customer-feedback graph. The provider itself advertises an
// `mcp:write` scope. An operator reviewing this wizard sees ten reads and
// no changes, which is not what the surface is.
expect(completed.actions.canMakeChanges).toEqual([]);
expect(completed.actions.readOnly.map((action) => action.toolName)).toEqual(
VENDOR_SHAPED_TOOLS.map((tool) => tool.name),
);
expect(
completed.catalog
.filter((entry) => ["run_graph_query", "execute_cypher_query"].includes(entry.toolName))
.map((entry) => [entry.toolName, entry.riskLevel, entry.isDestructive]),
).toEqual([
["run_graph_query", "read", false],
["execute_cypher_query", "read", false],
]);
const readEntry = completed.catalog.find((entry) => entry.toolName === "get_graph_schema")!;
await service.finishGalleryAppConnection(
company.id,
connected.connectionId,
{
enabledCatalogEntryIds: [readEntry.id],
askFirstCatalogEntryIds: [],
access: { agentIds: [agent!.id] },
},
{ actorType: "user", actorId: "board-user" },
);
const decisionInput = {
companyId: company.id,
actor: { actorType: "agent" as const, actorId: agent!.id, agentId: agent!.id },
request: {
connectionId: connected.connectionId,
catalogEntryId: readEntry.id,
toolName: "get_graph_schema",
},
};
await expect(policy.decide(decisionInput)).resolves.toMatchObject({
allowed: true,
reasonCode: "allow_profile",
});
await service.archiveConnection(connected.connectionId, company.id);
await expect(policy.decide(decisionInput)).resolves.toMatchObject({ allowed: false });
// Revocation is enforced entirely on the Paperclip side. Nothing was sent
// to the authorization server — there is no advertised revocation
// endpoint, and Paperclip never looks for one, so the access token stays
// valid at the vendor until it expires on its own. That residual is an
// operator step at the vendor, not something this path can close.
expect(fixture.requestsMatching(/revo(ke|cation)|introspect/i)).toHaveLength(0);
expect(Object.keys(VENDOR_SHAPED_AUTHORIZATION_SERVER_METADATA))
.not.toContain("revocation_endpoint");
});
it("gives a pasted config the generic treatment, with none of a catalog entry's curation", async () => {
installVendorShapedFixture();
const company = await createCompany(db);
const app = createRouteApp(db);
// "Paste a config" — the JSON an operator copies out of a vendor's docs.
const preview = await request(app)
.post(`/api/companies/${company.id}/tools/mcp/import-json`)
.send({ mcpJson: { mcpServers: { enterpret: { url: VENDOR_SHAPED_MCP_URL } } } })
.expect(200);
expect(preview.body.drafts).toEqual([
expect.objectContaining({
name: "enterpret",
transport: "mcp_remote",
status: "draft",
config: { url: VENDOR_SHAPED_MCP_URL },
}),
]);
// "Connect your own MCP server" — the same endpoint the wizard posts to.
const connect = await request(app)
.post(`/api/companies/${company.id}/tools/apps/connect`)
.send({ link: VENDOR_SHAPED_MCP_URL, name: "enterpret" })
.expect(201);
expect(connect.body.auth).toMatchObject({ kind: "oauth", issuer: VENDOR_SHAPED_ISSUER });
const [connection] = await db.select().from(toolConnections);
// What the operator gets with no code change, and what they do not get:
// the server is flagged unverified, and — the part that matters — new
// tools it advertises later are *not* quarantined for review.
expect(connection!.config).toMatchObject({
unverifiedServer: true,
quarantineNewEntries: false,
});
// An application row *is* created, so the connection is governed like any
// other. What it does not carry is the curation a catalog definition
// supplies — compare field for field against
// `packages/shared/src/app-definitions/notion.json`, which ships
// `branding.logoUrl`, `categories`, `featured`, `riskTier`,
// `requiredResourceFilters`, `ownershipModes`, `guidanceMd` and
// `redirectConstraints`. None of them has anywhere to come from here.
const [application] = await db
.select()
.from(toolApplications)
.where(eq(toolApplications.id, connection!.applicationId));
expect(application).toMatchObject({
// The operator's typed name is the only identity there is.
name: "enterpret",
});
// The key is synthesized per connection, not a reviewed gallery slug like
// notion.json's `"slug": "notion"`, so two operators pasting the same URL
// get two unrelated applications and nothing in the product knows they
// are the same vendor.
expect(application!.applicationKey).toMatch(/^app-gallery:link:[0-9a-f-]{36}$/);
for (const curatedField of [
"branding",
"categories",
"featured",
"riskTier",
"requiredResourceFilters",
"ownershipModes",
"guidanceMd",
"redirectConstraints",
]) {
expect(application!.metadata).not.toHaveProperty(curatedField);
expect(connection!.config).not.toHaveProperty(curatedField);
}
});
});
});