@12-apps/mcp 3.11.0 → 3.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-VDD4YRNP.js +197 -0
- package/dist/chunk-VDD4YRNP.js.map +1 -0
- package/dist/{create-api-mcp-oauth-CwVXKK-A.d.ts → create-api-mcp-oauth-CsC0jlH7.d.ts} +1 -1
- package/dist/generate/index.d.ts +1 -1
- package/dist/{generate-Dx3cK8th.d.ts → generate-BCDqBUjZ.d.ts} +1 -1
- package/dist/hono/index.d.ts +1 -1
- package/dist/index.d.ts +95 -3
- package/dist/index.js +41 -8
- package/dist/index.js.map +1 -1
- package/dist/manifest/index.d.ts +93 -0
- package/dist/manifest/index.js +22 -0
- package/dist/manifest/index.js.map +1 -0
- package/dist/manifest/server.d.ts +74 -0
- package/dist/manifest/server.js +41 -0
- package/dist/manifest/server.js.map +1 -0
- package/dist/oauth/index.d.ts +2 -2
- package/dist/oauth/index.js +6 -189
- package/dist/oauth/index.js.map +1 -1
- package/package.json +33 -14
- package/src/index.ts +10 -1
- package/src/manifest/index.ts +89 -0
- package/src/manifest/server.ts +94 -0
- package/src/openapi/annotations.ts +106 -0
- package/src/openapi/endpoint.ts +36 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import {
|
|
2
|
+
providerForHostId
|
|
3
|
+
} from "./chunk-FBUSSQSK.js";
|
|
4
|
+
import {
|
|
5
|
+
__name
|
|
6
|
+
} from "./chunk-7QVYU63E.js";
|
|
7
|
+
|
|
8
|
+
// src/oauth/prisma-stores.ts
|
|
9
|
+
function clientStore(getPrisma) {
|
|
10
|
+
return {
|
|
11
|
+
async create(client) {
|
|
12
|
+
const prisma = await getPrisma();
|
|
13
|
+
return prisma.oAuthClient.create({ data: client });
|
|
14
|
+
},
|
|
15
|
+
async findByClientId(clientId) {
|
|
16
|
+
const prisma = await getPrisma();
|
|
17
|
+
return prisma.oAuthClient.findUnique({ where: { clientId } });
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
__name(clientStore, "clientStore");
|
|
22
|
+
function refreshTokenStore(getPrisma) {
|
|
23
|
+
return {
|
|
24
|
+
async create(token) {
|
|
25
|
+
const prisma = await getPrisma();
|
|
26
|
+
await prisma.oAuthRefreshToken.create({ data: token });
|
|
27
|
+
},
|
|
28
|
+
async findByHash(tokenHash) {
|
|
29
|
+
const prisma = await getPrisma();
|
|
30
|
+
return prisma.oAuthRefreshToken.findUnique({ where: { tokenHash } });
|
|
31
|
+
},
|
|
32
|
+
async hasSuccessor(tokenHash) {
|
|
33
|
+
const prisma = await getPrisma();
|
|
34
|
+
const successor = await prisma.oAuthRefreshToken.findFirst({
|
|
35
|
+
where: { rotatedFrom: tokenHash }
|
|
36
|
+
});
|
|
37
|
+
return successor !== null;
|
|
38
|
+
},
|
|
39
|
+
async listFamily(userEmail, clientId) {
|
|
40
|
+
const prisma = await getPrisma();
|
|
41
|
+
return prisma.oAuthRefreshToken.findMany({ where: { userEmail, clientId } });
|
|
42
|
+
},
|
|
43
|
+
async revokeHashes(tokenHashes, at) {
|
|
44
|
+
if (tokenHashes.length === 0) return;
|
|
45
|
+
const prisma = await getPrisma();
|
|
46
|
+
await prisma.oAuthRefreshToken.updateMany({
|
|
47
|
+
where: { tokenHash: { in: [...tokenHashes] } },
|
|
48
|
+
data: { revokedAt: at }
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
async rotate(successor, parentHash, at) {
|
|
52
|
+
const prisma = await getPrisma();
|
|
53
|
+
return prisma.$transaction(async (tx) => {
|
|
54
|
+
const { count } = await tx.oAuthRefreshToken.updateMany({
|
|
55
|
+
where: { tokenHash: parentHash, revokedAt: null },
|
|
56
|
+
data: { revokedAt: at }
|
|
57
|
+
});
|
|
58
|
+
if (count !== 1) return false;
|
|
59
|
+
await tx.oAuthRefreshToken.create({ data: successor });
|
|
60
|
+
return true;
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
async revokeLiveForClient(userEmail, clientId) {
|
|
64
|
+
const prisma = await getPrisma();
|
|
65
|
+
const { count } = await prisma.oAuthRefreshToken.updateMany({
|
|
66
|
+
where: { userEmail, clientId, revokedAt: null },
|
|
67
|
+
data: { revokedAt: /* @__PURE__ */ new Date() }
|
|
68
|
+
});
|
|
69
|
+
return count;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
__name(refreshTokenStore, "refreshTokenStore");
|
|
74
|
+
var CONNECTION_SELECT = {
|
|
75
|
+
oauthClientId: true,
|
|
76
|
+
clientName: true,
|
|
77
|
+
host: true,
|
|
78
|
+
connectedAt: true,
|
|
79
|
+
lastActiveAt: true
|
|
80
|
+
};
|
|
81
|
+
function connectionStore(getPrisma) {
|
|
82
|
+
return {
|
|
83
|
+
async lastActiveAt(userId, oauthClientId) {
|
|
84
|
+
const prisma = await getPrisma();
|
|
85
|
+
const row = await prisma.mcpConnection.findUnique({
|
|
86
|
+
where: { userId_oauthClientId: { userId, oauthClientId } },
|
|
87
|
+
select: { lastActiveAt: true }
|
|
88
|
+
});
|
|
89
|
+
return row?.lastActiveAt ?? null;
|
|
90
|
+
},
|
|
91
|
+
async recordActivity({ userId, oauthClientId, clientName, host, at }) {
|
|
92
|
+
const prisma = await getPrisma();
|
|
93
|
+
await prisma.mcpConnection.upsert({
|
|
94
|
+
where: { userId_oauthClientId: { userId, oauthClientId } },
|
|
95
|
+
create: { userId, oauthClientId, clientName, host, connectedAt: at, lastActiveAt: at },
|
|
96
|
+
// Never blank a known host on refresh — keep the existing attribution
|
|
97
|
+
// when this grant cannot derive one.
|
|
98
|
+
update: {
|
|
99
|
+
clientName,
|
|
100
|
+
lastActiveAt: at,
|
|
101
|
+
revokedAt: null,
|
|
102
|
+
...host ? { host } : {}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
async listActive(userId) {
|
|
107
|
+
const prisma = await getPrisma();
|
|
108
|
+
const rows = await prisma.mcpConnection.findMany({
|
|
109
|
+
where: { userId, revokedAt: null },
|
|
110
|
+
orderBy: { lastActiveAt: "desc" },
|
|
111
|
+
select: { ...CONNECTION_SELECT }
|
|
112
|
+
});
|
|
113
|
+
return rows;
|
|
114
|
+
},
|
|
115
|
+
revokeByHost: /* @__PURE__ */ __name((userId, host) => revokeByHost(getPrisma, userId, host), "revokeByHost"),
|
|
116
|
+
announce: /* @__PURE__ */ __name((userId, host) => announce(getPrisma, userId, host), "announce")
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
__name(connectionStore, "connectionStore");
|
|
120
|
+
async function revokeByHost(getPrisma, userId, host) {
|
|
121
|
+
const prisma = await getPrisma();
|
|
122
|
+
const attributed = await prisma.mcpConnection.findMany({
|
|
123
|
+
where: { userId, revokedAt: null, host },
|
|
124
|
+
select: { id: true, oauthClientId: true }
|
|
125
|
+
});
|
|
126
|
+
const targets = attributed.length > 0 ? attributed : await prisma.mcpConnection.findMany({
|
|
127
|
+
where: { userId, revokedAt: null, host: null },
|
|
128
|
+
select: { id: true, oauthClientId: true }
|
|
129
|
+
});
|
|
130
|
+
if (targets.length === 0) return [];
|
|
131
|
+
await prisma.mcpConnection.updateMany({
|
|
132
|
+
where: { id: { in: targets.map((row) => String(row.id)) } },
|
|
133
|
+
data: { revokedAt: /* @__PURE__ */ new Date() }
|
|
134
|
+
});
|
|
135
|
+
return targets.map((row) => String(row.oauthClientId));
|
|
136
|
+
}
|
|
137
|
+
__name(revokeByHost, "revokeByHost");
|
|
138
|
+
async function announce(getPrisma, userId, host) {
|
|
139
|
+
const prisma = await getPrisma();
|
|
140
|
+
const now = /* @__PURE__ */ new Date();
|
|
141
|
+
const refreshed = await prisma.mcpConnection.updateMany({
|
|
142
|
+
where: { userId, revokedAt: null, host },
|
|
143
|
+
data: { lastActiveAt: now, revokedAt: null }
|
|
144
|
+
});
|
|
145
|
+
if (refreshed.count > 0) return refreshed.count;
|
|
146
|
+
const candidate = await prisma.mcpConnection.findFirst({
|
|
147
|
+
where: { userId, revokedAt: null, host: null },
|
|
148
|
+
orderBy: { lastActiveAt: "desc" },
|
|
149
|
+
select: { id: true }
|
|
150
|
+
});
|
|
151
|
+
if (!candidate) return 0;
|
|
152
|
+
await prisma.mcpConnection.update({
|
|
153
|
+
where: { id: candidate.id },
|
|
154
|
+
data: { host, lastActiveAt: now, revokedAt: null }
|
|
155
|
+
});
|
|
156
|
+
return 1;
|
|
157
|
+
}
|
|
158
|
+
__name(announce, "announce");
|
|
159
|
+
function createPrismaMcpStores(getPrisma) {
|
|
160
|
+
return {
|
|
161
|
+
clients: clientStore(getPrisma),
|
|
162
|
+
refreshTokens: refreshTokenStore(getPrisma),
|
|
163
|
+
connections: connectionStore(getPrisma)
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
__name(createPrismaMcpStores, "createPrismaMcpStores");
|
|
167
|
+
|
|
168
|
+
// src/oauth/connections.ts
|
|
169
|
+
function asProvider(host) {
|
|
170
|
+
return host === null ? null : providerForHostId(host);
|
|
171
|
+
}
|
|
172
|
+
__name(asProvider, "asProvider");
|
|
173
|
+
async function listAiConnections(connections, userId) {
|
|
174
|
+
const rows = await connections.listActive(userId);
|
|
175
|
+
return rows.map((row) => ({ ...row, host: asProvider(row.host) }));
|
|
176
|
+
}
|
|
177
|
+
__name(listAiConnections, "listAiConnections");
|
|
178
|
+
async function disconnectAiHost(stores, caller, host) {
|
|
179
|
+
const disconnectedClientIds = await stores.connections.revokeByHost(caller.userId, host);
|
|
180
|
+
const revoked = await Promise.all(
|
|
181
|
+
disconnectedClientIds.map(
|
|
182
|
+
(clientId) => stores.refreshTokens.revokeLiveForClient(caller.email, clientId)
|
|
183
|
+
)
|
|
184
|
+
);
|
|
185
|
+
return {
|
|
186
|
+
disconnectedClientIds,
|
|
187
|
+
revokedRefreshTokens: revoked.reduce((total, count) => total + count, 0)
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
__name(disconnectAiHost, "disconnectAiHost");
|
|
191
|
+
|
|
192
|
+
export {
|
|
193
|
+
createPrismaMcpStores,
|
|
194
|
+
listAiConnections,
|
|
195
|
+
disconnectAiHost
|
|
196
|
+
};
|
|
197
|
+
//# sourceMappingURL=chunk-VDD4YRNP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/oauth/prisma-stores.ts","../src/oauth/connections.ts"],"sourcesContent":["import type {\n McpConnectionStore,\n McpOauthStores,\n NewOAuthClient,\n NewRefreshToken,\n OAuthClientStore,\n RefreshTokenStore,\n StoredMcpConnection,\n StoredOAuthClient,\n StoredRefreshToken,\n} from \"./stores\";\n\n/**\n * The ports of `./stores.ts`, filled by Prisma (12-23).\n *\n * The package owns the three models (`prisma/mcp.prisma`), so their delegate\n * shapes are known and this adapter can be exact. A host with Prisma therefore\n * writes ONE line —\n *\n * stores: createPrismaMcpStores(async () => getPrismaClient() as unknown as McpOauthPrisma)\n *\n * — and no host code at all beyond it. The client is duck-typed (only the\n * delegates used, only the arguments used) so this file never imports a project's\n * generated client, and a non-Prisma host fills the ports directly instead.\n */\n\n/** A `where` on the composite unique of `mcp_connections`. */\ninterface ConnectionKey {\n userId_oauthClientId: { userId: string; oauthClientId: string };\n}\n\n/** The minimal Prisma surface the AS needs. Every field is one the surface writes. */\nexport interface McpOauthPrisma {\n oAuthClient: {\n create(args: { data: NewOAuthClient }): Promise<StoredOAuthClient>;\n findUnique(args: { where: { clientId: string } }): Promise<StoredOAuthClient | null>;\n };\n oAuthRefreshToken: {\n create(args: { data: NewRefreshToken }): Promise<unknown>;\n findUnique(args: { where: { tokenHash: string } }): Promise<StoredRefreshToken | null>;\n findFirst(args: { where: { rotatedFrom: string } }): Promise<{ tokenHash: string } | null>;\n findMany(args: {\n where: { userEmail: string; clientId: string };\n }): Promise<StoredRefreshToken[]>;\n // No single-row `update`: the rotation used to revoke its parent with one and\n // that was the bug (unconditional, so two concurrent rotations both won). Every\n // revoke here is now an `updateMany` with a predicate that says WHICH rows may\n // move, which is also why this delegate list stays honest about what is written.\n updateMany(args: {\n where:\n | { tokenHash: { in: string[] } }\n | { userEmail: string; clientId: string; revokedAt: null };\n data: { revokedAt: Date };\n }): Promise<{ count: number }>;\n };\n mcpConnection: {\n findUnique(args: {\n where: ConnectionKey;\n select: { lastActiveAt: true };\n }): Promise<{ lastActiveAt: Date } | null>;\n findFirst(args: {\n where: { userId: string; revokedAt: null; host: null };\n orderBy: { lastActiveAt: \"desc\" };\n select: { id: true };\n }): Promise<{ id: string } | null>;\n findMany(args: {\n where: { userId: string; revokedAt: null; host?: string | null };\n orderBy?: { lastActiveAt: \"desc\" };\n select: Record<string, true>;\n }): Promise<Record<string, unknown>[]>;\n upsert(args: {\n where: ConnectionKey;\n create: Record<string, unknown>;\n update: Record<string, unknown>;\n }): Promise<unknown>;\n update(args: { where: { id: string }; data: Record<string, unknown> }): Promise<unknown>;\n updateMany(args: {\n where: { id: { in: string[] } } | { userId: string; revokedAt: null; host: string };\n data: Record<string, unknown>;\n }): Promise<{ count: number }>;\n };\n /**\n * Prisma's INTERACTIVE transaction, used for the rotation's claim + write. The\n * callback form (not the array form) is required: the successor may only be\n * created once the conditional revoke has reported that it, and not a concurrent\n * sibling, claimed the parent — see `RefreshTokenStore.rotate`.\n */\n $transaction<T>(fn: (tx: McpOauthTx) => Promise<T>): Promise<T>;\n}\n\n/**\n * The delegate subset used INSIDE the rotation transaction. Not exported: it is\n * reachable structurally through `McpOauthPrisma.$transaction`, so no host ever\n * needs to name it, and exporting a type nobody imports is what knip flags.\n */\ninterface McpOauthTx {\n oAuthRefreshToken: {\n create(args: { data: NewRefreshToken }): Promise<unknown>;\n updateMany(args: {\n where: { tokenHash: string; revokedAt: null };\n data: { revokedAt: Date };\n }): Promise<{ count: number }>;\n };\n}\n\n/** A lazily-resolved client, so a host's singleton is awaited per call. */\nexport type McpOauthPrismaProvider = () => Promise<McpOauthPrisma>;\n\nfunction clientStore(getPrisma: McpOauthPrismaProvider): OAuthClientStore {\n return {\n async create(client: NewOAuthClient) {\n const prisma = await getPrisma();\n return prisma.oAuthClient.create({ data: client });\n },\n async findByClientId(clientId: string) {\n const prisma = await getPrisma();\n return prisma.oAuthClient.findUnique({ where: { clientId } });\n },\n };\n}\n\nfunction refreshTokenStore(getPrisma: McpOauthPrismaProvider): RefreshTokenStore {\n return {\n async create(token) {\n const prisma = await getPrisma();\n await prisma.oAuthRefreshToken.create({ data: token });\n },\n async findByHash(tokenHash) {\n const prisma = await getPrisma();\n return prisma.oAuthRefreshToken.findUnique({ where: { tokenHash } });\n },\n async hasSuccessor(tokenHash) {\n const prisma = await getPrisma();\n const successor = await prisma.oAuthRefreshToken.findFirst({\n where: { rotatedFrom: tokenHash },\n });\n return successor !== null;\n },\n async listFamily(userEmail, clientId) {\n const prisma = await getPrisma();\n return prisma.oAuthRefreshToken.findMany({ where: { userEmail, clientId } });\n },\n async revokeHashes(tokenHashes, at) {\n if (tokenHashes.length === 0) return;\n const prisma = await getPrisma();\n await prisma.oAuthRefreshToken.updateMany({\n where: { tokenHash: { in: [...tokenHashes] } },\n data: { revokedAt: at },\n });\n },\n async rotate(successor, parentHash, at) {\n const prisma = await getPrisma();\n return prisma.$transaction(async (tx) => {\n // CLAIM-ONCE. The `revokedAt: null` predicate is what makes this safe under\n // concurrency, and it is load-bearing rather than defensive: on Postgres's\n // default READ COMMITTED, a second transaction's `updateMany` blocks on the\n // row lock, then re-evaluates this WHERE against the COMMITTED row — which\n // now has a `revokedAt` — and reports 0 rows. So exactly one caller can ever\n // see count 1, and it is the only one that goes on to create a successor.\n // An unconditional `update` would let both through: two live successors of\n // one parent, and replay detection silently defeated (it waits for a third\n // use of the parent that now never comes).\n const { count } = await tx.oAuthRefreshToken.updateMany({\n where: { tokenHash: parentHash, revokedAt: null },\n data: { revokedAt: at },\n });\n // Lost the claim: write NOTHING. The zero-row update commits as the no-op\n // it is, so there is nothing to roll back.\n if (count !== 1) return false;\n // Same transaction as the claim, so a crash cannot leave a live parent AND\n // a live child either.\n await tx.oAuthRefreshToken.create({ data: successor });\n return true;\n });\n },\n async revokeLiveForClient(userEmail, clientId) {\n const prisma = await getPrisma();\n const { count } = await prisma.oAuthRefreshToken.updateMany({\n where: { userEmail, clientId, revokedAt: null },\n data: { revokedAt: new Date() },\n });\n return count;\n },\n };\n}\n\n/** The connection columns the account surface reads. */\nconst CONNECTION_SELECT = {\n oauthClientId: true,\n clientName: true,\n host: true,\n connectedAt: true,\n lastActiveAt: true,\n} as const;\n\nfunction connectionStore(getPrisma: McpOauthPrismaProvider): McpConnectionStore {\n return {\n async lastActiveAt(userId, oauthClientId) {\n const prisma = await getPrisma();\n const row = await prisma.mcpConnection.findUnique({\n where: { userId_oauthClientId: { userId, oauthClientId } },\n select: { lastActiveAt: true },\n });\n return row?.lastActiveAt ?? null;\n },\n async recordActivity({ userId, oauthClientId, clientName, host, at }) {\n const prisma = await getPrisma();\n await prisma.mcpConnection.upsert({\n where: { userId_oauthClientId: { userId, oauthClientId } },\n create: { userId, oauthClientId, clientName, host, connectedAt: at, lastActiveAt: at },\n // Never blank a known host on refresh — keep the existing attribution\n // when this grant cannot derive one.\n update: {\n clientName,\n lastActiveAt: at,\n revokedAt: null,\n ...(host ? { host } : {}),\n },\n });\n },\n async listActive(userId) {\n const prisma = await getPrisma();\n const rows = await prisma.mcpConnection.findMany({\n where: { userId, revokedAt: null },\n orderBy: { lastActiveAt: \"desc\" },\n select: { ...CONNECTION_SELECT },\n });\n return rows as unknown as StoredMcpConnection[];\n },\n revokeByHost: (userId, host) => revokeByHost(getPrisma, userId, host),\n announce: (userId, host) => announce(getPrisma, userId, host),\n };\n}\n\n/**\n * Disconnect one provider's connections — and ONLY that provider's.\n *\n * Every read and write is scoped by `userId`: a connection is per-user (an MCP\n * bearer is not tenant-scoped), so the user id IS the isolation here, and the\n * `id` list passed to the update comes from a query that already applied it.\n */\nasync function revokeByHost(\n getPrisma: McpOauthPrismaProvider,\n userId: string,\n host: string,\n): Promise<string[]> {\n const prisma = await getPrisma();\n const attributed = await prisma.mcpConnection.findMany({\n where: { userId, revokedAt: null, host },\n select: { id: true, oauthClientId: true },\n });\n // A legacy `host = null` row is claimed only when the provider has no row of\n // its own: pre-attribution connections must stay disconnectable, but a provider\n // that DID attribute can never revoke another assistant's row.\n const targets =\n attributed.length > 0\n ? attributed\n : await prisma.mcpConnection.findMany({\n where: { userId, revokedAt: null, host: null },\n select: { id: true, oauthClientId: true },\n });\n if (targets.length === 0) return [];\n await prisma.mcpConnection.updateMany({\n where: { id: { in: targets.map((row) => String(row.id)) } },\n data: { revokedAt: new Date() },\n });\n return targets.map((row) => String(row.oauthClientId));\n}\n\n/** A provider's self-report: refresh its own row, or claim the unattributed one. */\nasync function announce(\n getPrisma: McpOauthPrismaProvider,\n userId: string,\n host: string,\n): Promise<number> {\n const prisma = await getPrisma();\n const now = new Date();\n const refreshed = await prisma.mcpConnection.updateMany({\n where: { userId, revokedAt: null, host },\n data: { lastActiveAt: now, revokedAt: null },\n });\n if (refreshed.count > 0) return refreshed.count;\n\n // No row for this provider yet — attribute the just-connected one. Scoped by\n // user, so a self-report can never reach another account's connection.\n const candidate = await prisma.mcpConnection.findFirst({\n where: { userId, revokedAt: null, host: null },\n orderBy: { lastActiveAt: \"desc\" },\n select: { id: true },\n });\n if (!candidate) return 0;\n await prisma.mcpConnection.update({\n where: { id: candidate.id },\n data: { host, lastActiveAt: now, revokedAt: null },\n });\n return 1;\n}\n\n/** Every port, over one lazily-resolved Prisma client. */\nexport function createPrismaMcpStores(getPrisma: McpOauthPrismaProvider): McpOauthStores {\n return {\n clients: clientStore(getPrisma),\n refreshTokens: refreshTokenStore(getPrisma),\n connections: connectionStore(getPrisma),\n };\n}\n","import { providerForHostId, type AiProvider } from \"../guide\";\nimport type {\n McpConnectionStore,\n RefreshTokenStore,\n StoredMcpConnection,\n} from \"./stores\";\n\n/**\n * The account surface's connection OPERATIONS (12-48) — the half of the\n * `GET/DELETE /api/account/mcp-connections` endpoints that is contract rather\n * than host vocabulary.\n *\n * The ROUTE stays in the host on purpose: it mixes the host's session\n * resolution, its response envelope, its published plugin URLs and its logger,\n * and injecting all four here would make the config surface bigger than the\n * handler it replaces. What must NOT stay in each host is the disconnect's\n * both-halves rule, because getting it half right LOOKS right:\n *\n * `connections.revokeByHost` ends the connection rows and returns the OAuth\n * client ids behind them — and a host that stops there has revoked nothing that\n * matters. The assistant still holds a live refresh token for each of those\n * clients, rotates it on schedule, and the very next grant records fresh\n * activity: the card the user just disconnected lights green again on its own.\n * So the rule is one function: revoke the rows AND end every live refresh token\n * of each returned client, in the same call, with no way to import one half\n * without the other.\n *\n * Deliberately NOT invalidated here: the assistant's current ACCESS token.\n * Those are self-contained JWTs the server does not track; a just-disconnected\n * host keeps working for at most their TTL (15 minutes by default) and can then\n * obtain nothing further.\n */\n\n/** An active AI connection, narrowed for display. */\nexport interface AiConnectionSnapshot {\n oauthClientId: string;\n clientName: string | null;\n /** The provider this connection is attributed to (`null` = pre-attribution). */\n host: AiProvider | null;\n connectedAt: Date;\n lastActiveAt: Date;\n}\n\n/** The caller the operations act for — always the session's own user. */\nexport interface AiConnectionCaller {\n /** The host's user id — what `mcp_connections` rows are keyed by. */\n userId: string;\n /** The identity refresh tokens are bound to (the AS binds by email). */\n email: string;\n}\n\n/** What one disconnect actually ended, for the host's log and response. */\nexport interface AiDisconnectResult {\n /** OAuth client ids whose connection rows were revoked. */\n disconnectedClientIds: string[];\n /** Live refresh tokens ended across those clients — the half that cuts access. */\n revokedRefreshTokens: number;\n}\n\n/** Narrow a stored `host` string to a known provider, or `null`. */\nfunction asProvider(host: string | null): AiProvider | null {\n return host === null ? null : providerForHostId(host);\n}\n\n/**\n * A user's active connections, most-recently-active first, with the stored open\n * `host` string narrowed to the package's closed {@link AiProvider} union — the\n * store cannot know which assistants have screens, but the union is this\n * package's own vocabulary (`guide.ts`), so the narrowing lives beside it\n * rather than being re-derived in every host.\n */\nexport async function listAiConnections(\n connections: McpConnectionStore,\n userId: string,\n): Promise<AiConnectionSnapshot[]> {\n const rows: StoredMcpConnection[] = await connections.listActive(userId);\n return rows.map((row) => ({ ...row, host: asProvider(row.host) }));\n}\n\n/**\n * Disconnect one provider for this user — BOTH halves, atomically from the\n * caller's point of view (see the module doc for why one half alone is a\n * disconnect that undoes itself).\n *\n * Idempotent: disconnecting a provider that was never connected returns zero\n * counts rather than failing, so a double-click is harmless. Repeat calls also\n * report zero — `revokeLiveForClient` skips already-revoked tokens by contract.\n */\nexport async function disconnectAiHost(\n stores: { connections: McpConnectionStore; refreshTokens: RefreshTokenStore },\n caller: AiConnectionCaller,\n host: AiProvider,\n): Promise<AiDisconnectResult> {\n const disconnectedClientIds = await stores.connections.revokeByHost(caller.userId, host);\n const revoked = await Promise.all(\n disconnectedClientIds.map((clientId) =>\n stores.refreshTokens.revokeLiveForClient(caller.email, clientId),\n ),\n );\n return {\n disconnectedClientIds,\n revokedRefreshTokens: revoked.reduce((total, count) => total + count, 0),\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -644,4 +644,4 @@ interface ApiMcpOauth {
|
|
|
644
644
|
}
|
|
645
645
|
declare function createApiMcpOauth(config: McpOauthConfig): ApiMcpOauth;
|
|
646
646
|
|
|
647
|
-
export { trustedOriginsFromEnv as $, type ApiMcpOauth as A, type StoredMcpConnection as B, type CodeReplayStore as C, DEFAULT_MCP_RESOURCE_PATH as D, type VerifyAccessTokenOptions as E, createApiMcpOauth as F, hashSecret as G, inProcessCodeReplayStore as H, issuer as I, loadSigningKeyFromEnv as J, matchesRedirectUri as K, originFromRequest as L, type McpOauthConfig as M, type NewOAuthClient as N, type OAuthClientStore as O, type ProviderAttributionRule as P, providerFromRedirectUris as Q, type RefreshTokenStore as R, type StoredOAuthClient as S, type TokenEndpointAuthMethod as T, registerClient as U, type VerifiedAccessToken as V, resolveMcpOauthConfig as W, resolveTrustedOrigin as X, resourceAudience as Y, signAccessToken as Z, signingKeyProvider as _, type
|
|
647
|
+
export { trustedOriginsFromEnv as $, type ApiMcpOauth as A, type StoredMcpConnection as B, type CodeReplayStore as C, DEFAULT_MCP_RESOURCE_PATH as D, type VerifyAccessTokenOptions as E, createApiMcpOauth as F, hashSecret as G, inProcessCodeReplayStore as H, issuer as I, loadSigningKeyFromEnv as J, matchesRedirectUri as K, originFromRequest as L, type McpOauthConfig as M, type NewOAuthClient as N, type OAuthClientStore as O, type ProviderAttributionRule as P, providerFromRedirectUris as Q, type RefreshTokenStore as R, type StoredOAuthClient as S, type TokenEndpointAuthMethod as T, registerClient as U, type VerifiedAccessToken as V, resolveMcpOauthConfig as W, resolveTrustedOrigin as X, resourceAudience as Y, signAccessToken as Z, signingKeyProvider as _, type McpOauthRoute as a, verifyAccessToken as a0, type McpSigningKeyProvider as b, type NewRefreshToken as c, type StoredRefreshToken as d, type McpOauthStores as e, type McpConnectionStore as f, ACCESS_TOKEN_TTL_SECONDS as g, AccessTokenError as h, type AccessTokenErrorCode as i, DEFAULT_OAUTH_PATHS as j, DEFAULT_PROVIDER_ROOTS as k, DEFAULT_SIGNING_KEY_ENV as l, DEFAULT_SIGNING_KEY_ID_ENV as m, MCP_SUPPORTED_SCOPES as n, type McpConnectionRecording as o, type McpOauthContext as p, type McpOauthHandlers as q, type McpOauthPaths as r, type McpOauthSession as s, type McpScope as t, type McpSigningKey as u, type PublicSigningJwk as v, type RegisterClientInput as w, type RegisteredClient as x, SIGNING_ALG as y, type SignAccessTokenInput as z };
|
package/dist/generate/index.d.ts
CHANGED
|
@@ -181,4 +181,4 @@ interface OpenApiDocument {
|
|
|
181
181
|
*/
|
|
182
182
|
declare function generateTools(doc: OpenApiDocument, options?: GenerateOptions): GeneratedTool[];
|
|
183
183
|
|
|
184
|
-
export { type AuthResolver as A, type DispatchConfig as D, type GeneratedTool as G, type JsonSchema as J, type OpenApiDocument as O, type ParameterLocation as P, type RequestAuth as R, type ToolManifest as T, type
|
|
184
|
+
export { type AuthResolver as A, type DispatchConfig as D, type GeneratedTool as G, type JsonSchema as J, type OpenApiDocument as O, type ParameterLocation as P, type RequestAuth as R, type ToolManifest as T, type ToolAnnotations as a, type DispatchResult as b, type GenerateOptions as c, type OpenApiOperation as d, type OpenApiParameter as e, type OpenApiRequestBody as f, type OpenApiResponse as g, type ToolParameter as h, generateTools as i };
|
package/dist/hono/index.d.ts
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { J as JsonSchema,
|
|
2
|
-
export { A as AuthResolver, c as GenerateOptions, O as OpenApiDocument, d as OpenApiOperation, e as OpenApiParameter, f as OpenApiRequestBody, g as OpenApiResponse, P as ParameterLocation, h as ToolParameter, i as generateTools } from './generate-
|
|
1
|
+
import { J as JsonSchema, a as ToolAnnotations, G as GeneratedTool, D as DispatchConfig, b as DispatchResult, R as RequestAuth, T as ToolManifest } from './generate-BCDqBUjZ.js';
|
|
2
|
+
export { A as AuthResolver, c as GenerateOptions, O as OpenApiDocument, d as OpenApiOperation, e as OpenApiParameter, f as OpenApiRequestBody, g as OpenApiResponse, P as ParameterLocation, h as ToolParameter, i as generateTools } from './generate-BCDqBUjZ.js';
|
|
3
3
|
export { A as AiCapability, a as AiConnectPromptCopy, b as AiConnectPromptSpec, c as AiHostBrand, d as AiHostConfigureStage, e as AiHostGuide, f as AiHostLink, g as AiProvider, h as aiConnectPrompt, p as providerForHostId } from './guide-KQNcXlMG.js';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
export { A as AI_CAPABILITIES, a as AI_CONNECT_PROMPT, b as AI_HOST_GUIDES, c as AI_PERMISSION_MODEL, E as EN_US_AI_CAPABILITIES, d as EN_US_AI_CONNECT_PROMPT, e as EN_US_AI_HOST_GUIDES, f as EN_US_AI_PERMISSION_MODEL, P as PT_BR_AI_CAPABILITIES, g as PT_BR_AI_CONNECT_PROMPT, h as PT_BR_AI_HOST_GUIDES, i as PT_BR_AI_PERMISSION_MODEL } from './locales-eKE_OJw4.js';
|
|
@@ -51,6 +51,36 @@ declare function inlineSchemaRefs(schema: JsonSchema): JsonSchema;
|
|
|
51
51
|
*/
|
|
52
52
|
/** The methods an MCP-exposed route may use. */
|
|
53
53
|
type HttpMethod = "get" | "post" | "put" | "patch" | "delete";
|
|
54
|
+
/**
|
|
55
|
+
* What a PACKAGE can say about how its own tool behaves.
|
|
56
|
+
*
|
|
57
|
+
* The host still owns the final `ToolAnnotations` — every field required, and
|
|
58
|
+
* `mcp:lint` unchanged in demanding that each tool ends classified. What
|
|
59
|
+
* changes is who supplies the DEFAULT. A package declaring
|
|
60
|
+
* `getSupplierVersions` knows perfectly well that it reads and does not
|
|
61
|
+
* destroy; a host cannot know that without reading the package's source, so
|
|
62
|
+
* today it restates the classification by hand — 48 lines of policy hints for
|
|
63
|
+
* one package's eight-endpoint factory, growing with every collection plugged
|
|
64
|
+
* in, and wrong the moment the package changes a verb.
|
|
65
|
+
*
|
|
66
|
+
* Every field is OPTIONAL here, which is the whole difference from
|
|
67
|
+
* `ToolAnnotations`: this is a suggestion the host merges under its own table,
|
|
68
|
+
* so a package that knows two of the four says two and stays silent on the
|
|
69
|
+
* rest. Deliberately spelled without the `Hint` suffix and as a structural
|
|
70
|
+
* twin of `@12-apps/wiring`'s `WireMcpAnnotations`, so an `McpEndpoint` still
|
|
71
|
+
* satisfies `WireMcpTool` — restated rather than imported because this package
|
|
72
|
+
* takes no dependency on the wiring contract.
|
|
73
|
+
*/
|
|
74
|
+
interface McpAnnotationDefaults {
|
|
75
|
+
/** Human title override; hosts may re-derive from the operation id. */
|
|
76
|
+
title?: string;
|
|
77
|
+
/** The tool only reads — never mutates host state. */
|
|
78
|
+
readOnly?: boolean;
|
|
79
|
+
/** A destructive write (delete/purge), as opposed to an additive one. */
|
|
80
|
+
destructive?: boolean;
|
|
81
|
+
/** The tool reaches beyond the host's own data (external services). */
|
|
82
|
+
openWorld?: boolean;
|
|
83
|
+
}
|
|
54
84
|
interface McpEndpointBase {
|
|
55
85
|
/** Stable tool id — this becomes the MCP tool name, so renaming it is a
|
|
56
86
|
* breaking change for every agent that has learned the old one. */
|
|
@@ -67,6 +97,11 @@ interface McpEndpointBase {
|
|
|
67
97
|
params?: z.ZodType;
|
|
68
98
|
/** Request body schema (writes only). */
|
|
69
99
|
body?: z.ZodType;
|
|
100
|
+
/**
|
|
101
|
+
* Behavior the package can assert about its own tool. Optional, and merged
|
|
102
|
+
* UNDER the host's table — see {@link McpAnnotationDefaults}.
|
|
103
|
+
*/
|
|
104
|
+
annotations?: McpAnnotationDefaults;
|
|
70
105
|
}
|
|
71
106
|
/**
|
|
72
107
|
* A declared endpoint either answers 200 with a schema'd JSON body (the
|
|
@@ -88,6 +123,63 @@ type McpEndpoint = McpEndpointBase & ({
|
|
|
88
123
|
response?: never;
|
|
89
124
|
});
|
|
90
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Composing a tool's behavior classification out of two sources.
|
|
128
|
+
*
|
|
129
|
+
* The rule the host's gate enforces is unchanged: every served tool ends with
|
|
130
|
+
* a COMPLETE `ToolAnnotations` — a title and all three hints — and a tool that
|
|
131
|
+
* ends unclassified fails `mcp:lint`. ChatGPT App review treats a missing hint
|
|
132
|
+
* as a blocker, and the Anthropic connector directory derives auto-permissions
|
|
133
|
+
* from `readOnlyHint`/`destructiveHint`, so there is no defensible default for
|
|
134
|
+
* "we did not say".
|
|
135
|
+
*
|
|
136
|
+
* What this adds is where the answer may COME FROM. A package that declares
|
|
137
|
+
* `getSupplierVersions` knows it reads and does not destroy; the host cannot
|
|
138
|
+
* know that without reading the package's source, so it restated the
|
|
139
|
+
* classification by hand — one line per tool, per collection, wrong the moment
|
|
140
|
+
* the package changed a verb. Now the package can assert what it knows and the
|
|
141
|
+
* host's table becomes what it should always have been: OVERRIDES, plus the
|
|
142
|
+
* tools the host itself owns.
|
|
143
|
+
*
|
|
144
|
+
* ## Precedence, and why it runs this way
|
|
145
|
+
*
|
|
146
|
+
* The HOST wins every field it states. A package's claim is a default, not a
|
|
147
|
+
* fact about the host's deployment: the same endpoint can be read-only in one
|
|
148
|
+
* app and reach an external service in another (a host that proxies its
|
|
149
|
+
* catalog reads through a vendor), and the host is the only party that knows.
|
|
150
|
+
* Inverting this would make a package version bump silently re-classify a tool
|
|
151
|
+
* an operator had already audited — the exact thing an audited classification
|
|
152
|
+
* exists to prevent.
|
|
153
|
+
*
|
|
154
|
+
* ## What it refuses
|
|
155
|
+
*
|
|
156
|
+
* A field neither side supplies. `resolveToolAnnotations` throws naming the
|
|
157
|
+
* tool and the missing fields, which keeps the completeness property a
|
|
158
|
+
* REFUSAL rather than a lint pass over a table that quietly grew a gap. The
|
|
159
|
+
* host's own gate can keep its message; this one fires first and says the same
|
|
160
|
+
* thing in the same terms.
|
|
161
|
+
*/
|
|
162
|
+
|
|
163
|
+
/** The host's half — whatever it chose to state, per tool. */
|
|
164
|
+
type ToolAnnotationOverrides = Partial<ToolAnnotations>;
|
|
165
|
+
/**
|
|
166
|
+
* Merge a package's declared defaults under a host's overrides.
|
|
167
|
+
*
|
|
168
|
+
* The four fields are resolved into one record and checked generically rather
|
|
169
|
+
* than branch by branch — which keeps the "host wins, and `false` is an
|
|
170
|
+
* answer" rule stated exactly once per field instead of once per field per
|
|
171
|
+
* check.
|
|
172
|
+
*
|
|
173
|
+
* `??` and not `||` throughout, and that is the trap the whole merge turns on:
|
|
174
|
+
* `false` is a real classification — "this tool does not destroy" — and must
|
|
175
|
+
* not fall through to the package's answer.
|
|
176
|
+
*
|
|
177
|
+
* @param name the tool id, for the refusal message
|
|
178
|
+
* @param defaults what the package asserted (`McpEndpoint.annotations`)
|
|
179
|
+
* @param overrides what the host's own table says; wins every field it states
|
|
180
|
+
*/
|
|
181
|
+
declare function resolveToolAnnotations(name: string, defaults: McpAnnotationDefaults | undefined, overrides: ToolAnnotationOverrides | undefined): ToolAnnotations;
|
|
182
|
+
|
|
91
183
|
/** Raised when tool arguments cannot be routed onto the HTTP request. */
|
|
92
184
|
declare class DispatchInputError extends Error {
|
|
93
185
|
constructor(message: string);
|
|
@@ -469,4 +561,4 @@ interface AuthorizationServerMetadata {
|
|
|
469
561
|
*/
|
|
470
562
|
declare function buildAuthorizationServerMetadata(input: AuthorizationServerMetadataInput): AuthorizationServerMetadata;
|
|
471
563
|
|
|
472
|
-
export { type AuthorizationServerMetadata, type AuthorizationServerMetadataInput, type AuthorizationServerPaths, type BuildManifestOptions, DispatchConfig, DispatchInputError, DispatchResult, GeneratedTool, HTTP_STATUS_META_KEY, type HttpMethod, type JsonRpcRequest, type JsonRpcResponse, JsonSchema, MCP_PROTOCOL_VERSION, type McpEndpoint, type McpJsonRpcOptions, type McpServerInfo, type McpToolDescriptor, type McpToolResult, PROTECTED_RESOURCE_METADATA_PATH, type ProtectedResourceMetadata, type ProtectedResourceMetadataInput, type RegistryOptions, RequestAuth, type SurfaceLock, type SurfaceLockCheck, ToolAnnotations, ToolManifest, type ToolRegistry, UNAUTHORIZED_CODE, UnsupportedSchemaError, bearerChallenge, buildAuthorizationServerMetadata, buildManifest, buildProtectedResourceMetadata, createToolRegistry, dispatchTool, handleMcpJsonRpc, inlineSchemaRefs, redactResponseBody, redactResponseSchema, serializeManifest, serializeSurfaceLock, surfaceDigest, surfaceLockProblem };
|
|
564
|
+
export { type AuthorizationServerMetadata, type AuthorizationServerMetadataInput, type AuthorizationServerPaths, type BuildManifestOptions, DispatchConfig, DispatchInputError, DispatchResult, GeneratedTool, HTTP_STATUS_META_KEY, type HttpMethod, type JsonRpcRequest, type JsonRpcResponse, JsonSchema, MCP_PROTOCOL_VERSION, type McpAnnotationDefaults, type McpEndpoint, type McpJsonRpcOptions, type McpServerInfo, type McpToolDescriptor, type McpToolResult, PROTECTED_RESOURCE_METADATA_PATH, type ProtectedResourceMetadata, type ProtectedResourceMetadataInput, type RegistryOptions, RequestAuth, type SurfaceLock, type SurfaceLockCheck, type ToolAnnotationOverrides, ToolAnnotations, ToolManifest, type ToolRegistry, UNAUTHORIZED_CODE, UnsupportedSchemaError, bearerChallenge, buildAuthorizationServerMetadata, buildManifest, buildProtectedResourceMetadata, createToolRegistry, dispatchTool, handleMcpJsonRpc, inlineSchemaRefs, redactResponseBody, redactResponseSchema, resolveToolAnnotations, serializeManifest, serializeSurfaceLock, surfaceDigest, surfaceLockProblem };
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,14 @@ import {
|
|
|
12
12
|
PT_BR_AI_HOST_GUIDES,
|
|
13
13
|
PT_BR_AI_PERMISSION_MODEL
|
|
14
14
|
} from "./chunk-WUNMAHQG.js";
|
|
15
|
+
import {
|
|
16
|
+
buildManifest,
|
|
17
|
+
generateTools,
|
|
18
|
+
serializeManifest,
|
|
19
|
+
serializeSurfaceLock,
|
|
20
|
+
surfaceDigest,
|
|
21
|
+
surfaceLockProblem
|
|
22
|
+
} from "./chunk-HAZOPC6U.js";
|
|
15
23
|
import {
|
|
16
24
|
aiConnectPrompt,
|
|
17
25
|
providerForHostId
|
|
@@ -22,14 +30,6 @@ import {
|
|
|
22
30
|
buildAuthorizationServerMetadata,
|
|
23
31
|
buildProtectedResourceMetadata
|
|
24
32
|
} from "./chunk-WJJNKKNS.js";
|
|
25
|
-
import {
|
|
26
|
-
buildManifest,
|
|
27
|
-
generateTools,
|
|
28
|
-
serializeManifest,
|
|
29
|
-
serializeSurfaceLock,
|
|
30
|
-
surfaceDigest,
|
|
31
|
-
surfaceLockProblem
|
|
32
|
-
} from "./chunk-HAZOPC6U.js";
|
|
33
33
|
import {
|
|
34
34
|
__name
|
|
35
35
|
} from "./chunk-7QVYU63E.js";
|
|
@@ -94,6 +94,38 @@ function inlineSchemaRefs(schema) {
|
|
|
94
94
|
}
|
|
95
95
|
__name(inlineSchemaRefs, "inlineSchemaRefs");
|
|
96
96
|
|
|
97
|
+
// src/openapi/annotations.ts
|
|
98
|
+
function titleOf(candidate) {
|
|
99
|
+
return typeof candidate === "string" && candidate.trim() !== "" ? candidate : void 0;
|
|
100
|
+
}
|
|
101
|
+
__name(titleOf, "titleOf");
|
|
102
|
+
function resolveToolAnnotations(name, defaults, overrides) {
|
|
103
|
+
const host = overrides ?? {};
|
|
104
|
+
const declared = defaults ?? {};
|
|
105
|
+
const resolved = {
|
|
106
|
+
title: titleOf(host.title ?? declared.title),
|
|
107
|
+
readOnlyHint: host.readOnlyHint ?? declared.readOnly,
|
|
108
|
+
openWorldHint: host.openWorldHint ?? declared.openWorld,
|
|
109
|
+
destructiveHint: host.destructiveHint ?? declared.destructive
|
|
110
|
+
};
|
|
111
|
+
const missing = REQUIRED_FIELDS.filter((field) => resolved[field] === void 0);
|
|
112
|
+
if (missing.length > 0) refuse(name, missing);
|
|
113
|
+
return resolved;
|
|
114
|
+
}
|
|
115
|
+
__name(resolveToolAnnotations, "resolveToolAnnotations");
|
|
116
|
+
var REQUIRED_FIELDS = [
|
|
117
|
+
"title",
|
|
118
|
+
"readOnlyHint",
|
|
119
|
+
"openWorldHint",
|
|
120
|
+
"destructiveHint"
|
|
121
|
+
];
|
|
122
|
+
function refuse(name, missing) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`MCP tool "${name}" ends unclassified: neither the package nor the host supplied ${missing.join(", ")}. Every served tool needs a complete classification \u2014 a missing hint blocks ChatGPT App review, and the connector directory derives auto-permissions from readOnlyHint/destructiveHint.`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
__name(refuse, "refuse");
|
|
128
|
+
|
|
97
129
|
// src/dispatch/proxy.ts
|
|
98
130
|
var DispatchInputError = class extends Error {
|
|
99
131
|
static {
|
|
@@ -395,6 +427,7 @@ export {
|
|
|
395
427
|
providerForHostId,
|
|
396
428
|
redactResponseBody,
|
|
397
429
|
redactResponseSchema,
|
|
430
|
+
resolveToolAnnotations,
|
|
398
431
|
serializeManifest,
|
|
399
432
|
serializeSurfaceLock,
|
|
400
433
|
surfaceDigest,
|