(input: {
display_name: string;
description?: string;
resource_url: string;
/** Optional canonical key override (used by Browse connectors). */
key?: string;
})
| 260 | * `notfair-google-ads`). When omitted, the display name is slugified. |
| 261 | */ |
| 262 | export async function addUserMcpServerAction(input: { |
| 263 | display_name: string; |
| 264 | description?: string; |
| 265 | resource_url: string; |
| 266 | /** Optional canonical key override (used by Browse connectors). */ |
| 267 | key?: string; |
| 268 | }): Promise<AddUserMcpServerResult> { |
| 269 | const project = await getActiveProject(); |
| 270 | if (!project) { |
| 271 | return { |
| 272 | ok: false, |
| 273 | kind: "no_project", |
| 274 | error: "No active project. Pick one before adding an MCP server.", |
| 275 | }; |
| 276 | } |
| 277 | |
| 278 | let key: string; |
| 279 | if (input.key && KEY_PATTERN.test(input.key)) { |
| 280 | key = input.key; |
| 281 | } else { |
| 282 | const slug = slugify(input.display_name); |
| 283 | if (!slug.ok) { |
| 284 | return { |
| 285 | ok: false, |
| 286 | kind: "name_unusable", |
| 287 | error: `Can't derive a key from this name: ${slug.reason}.`, |
| 288 | }; |
| 289 | } |
| 290 | key = slug.slug; |
| 291 | } |
| 292 | |
| 293 | // Preset re-add: just un-hide. No row to write, no discovery probe. |
| 294 | if (isPresetKey(key)) { |
| 295 | const { removeHiddenMcpPresetKey } = await import("@/server/db/projects"); |
| 296 | removeHiddenMcpPresetKey(project.slug, key); |
| 297 | revalidatePath("/", "layout"); |
| 298 | return { ok: true, key }; |
| 299 | } |
| 300 | |
| 301 | // User row already exists for this key — Browse "click again" path. |
| 302 | if (findUserMcpServer(project.slug, key)) { |
| 303 | return { ok: true, key }; |
| 304 | } |
| 305 | |
| 306 | // Same MCP server (same URL) is already in this project under a |
| 307 | // different key — e.g. NotFair Meta Ads was added pre-canonical-id and |
| 308 | // saved as `notfair-meta-ads`, while the tile click would use |
| 309 | // `notfair-metaads`. Treat as an idempotent re-pick: return the |
| 310 | // existing key so the connect chain reuses it instead of writing a |
| 311 | // duplicate row. |
| 312 | const existingByUrl = findUserMcpServerByResourceUrl( |
| 313 | project.slug, |
| 314 | input.resource_url, |
| 315 | ); |
| 316 | if (existingByUrl) { |
| 317 | return { ok: true, key: existingByUrl.key }; |
| 318 | } |
| 319 |
no test coverage detected