diff --git a/adrs/specstream-mcp-vendor.md b/adrs/specstream-mcp-vendor.md new file mode 100644 index 000000000..77da3b223 --- /dev/null +++ b/adrs/specstream-mcp-vendor.md @@ -0,0 +1,16 @@ +# Add SpecStream as an MCP vendor + +I make SpecStream (https://specstream.ai/?utm_source=crm), so I'm biased. +You ask it for data in plain words and it returns rows from the live web, each value with its source URL. + +What made me notice: the CRM agent researches one person or company at a time (`context-dev.ts`, `perplexity.ts`). +That breaks down on a whole list, like "200 seed stage fintechs in Germany that hired a head of sales this quarter". +Every row must meet the conditions and every fact needs its page, so a rep can open row 147 without trusting the agent. + +What I'd do: add `packages/mcp-integrations/src/vendors/specstream.ts` and one line in `vendors/index.ts`, shaped like Linear and Notion. +The remote MCP (https://api.specstream.ai/mcp, OAuth with dynamic client registration) passes your `McpOAuthClient.discover`. +`finds:spend` is only in read-write, and tools that start paid work or send data out are in `riskyTools`, so each deployed agent needs an explicit grant. + +What it breaks: nothing, it's additive. +One more vendor on Connections, and usage is billed to the user's own SpecStream account; the CRM pays nothing. +The PR has the code (two small files and a test), happy to drop it if you'd rather settle the idea first. diff --git a/docs/connections.md b/docs/connections.md index 59196bcbf..ddce3149c 100644 --- a/docs/connections.md +++ b/docs/connections.md @@ -234,7 +234,7 @@ Docusign is spelled with a lowercase s since its 2024 rebrand. The note-taker is ## MCP connections -Clerk, Cloudflare, Linear, Neon, PlanetScale, Prisma, Supabase, and Vercel use the shared MCP connection flow. +Clerk, Cloudflare, Linear, Neon, PlanetScale, Prisma, SpecStream, Supabase, and Vercel use the shared MCP connection flow. An owner or admin chooses read-only or read-and-write access. The API stores OAuth tokens in encrypted form. The API never returns tokens to the browser. diff --git a/packages/mcp-integrations/src/vendors/index.ts b/packages/mcp-integrations/src/vendors/index.ts index 4ad6334e1..6dfd62dde 100644 --- a/packages/mcp-integrations/src/vendors/index.ts +++ b/packages/mcp-integrations/src/vendors/index.ts @@ -5,6 +5,7 @@ import { neon } from "./neon"; import { notion } from "./notion"; import { planetscale } from "./planetscale"; import { prisma } from "./prisma"; +import { specstream } from "./specstream"; import { supabase } from "./supabase"; import { vercel } from "./vercel"; @@ -16,6 +17,7 @@ export const vendors = [ notion, planetscale, prisma, + specstream, supabase, vercel, ] as const; diff --git a/packages/mcp-integrations/src/vendors/specstream.ts b/packages/mcp-integrations/src/vendors/specstream.ts new file mode 100644 index 000000000..042cc7380 --- /dev/null +++ b/packages/mcp-integrations/src/vendors/specstream.ts @@ -0,0 +1,56 @@ +import { parseVendor } from "../vendor"; + +const READ_SCOPES = [ + "specs:read", + "finds:read", + "projects:read", + "account:read", +] as const; + +export const specstream = parseVendor({ + slug: "specstream", + name: "SpecStream", + presentation: { + description: + "Let deployed agents find company data with a source on every value", + bringsIn: "Lists of companies and people, each value with its source URL", + sendsReadOnly: "Nothing", + sendsReadWrite: "Requests you approve, priced before they run", + }, + mcp: { + resource: "https://api.specstream.ai/mcp", + transport: "streamable-http", + }, + auth: { + kind: "mcp_oauth", + authorizationServers: ["https://specstream.ai"], + }, + access: { + read_only: { + url: "https://api.specstream.ai/mcp", + scopes: [...READ_SCOPES], + }, + read_write: { + url: "https://api.specstream.ai/mcp", + scopes: [...READ_SCOPES, "specs:write", "finds:write", "finds:spend"], + }, + }, + riskyTools: [ + "authorize_capacity_work", + "quote_capacity_work", + "create_capacity_schedule", + "update_capacity_schedule", + "send_to_destination", + "create_destination_sync", + "update_destination_sync", + "share_find", + "share_collection", + "export_find", + ], + docsHosts: ["specstream.ai"], + source: { + providerDocs: "https://specstream.ai/docs/mcp?utm_source=crm", + tools: "https://specstream.ai/docs/mcp?utm_source=crm", + checkedAt: "2026-11-04", + }, +}); diff --git a/packages/mcp-integrations/test/catalog.spec.ts b/packages/mcp-integrations/test/catalog.spec.ts index 3548d0bbf..e0ddd08e3 100644 --- a/packages/mcp-integrations/test/catalog.spec.ts +++ b/packages/mcp-integrations/test/catalog.spec.ts @@ -14,6 +14,7 @@ describe("MCP catalog", () => { "notion", "planetscale", "prisma", + "specstream", "supabase", "vercel", ]); @@ -32,6 +33,24 @@ describe("MCP catalog", () => { expect(vendorAccess(notion, "read_only").scopes).toEqual(["default"]); }); + test("registers SpecStream with spend gated behind its own scope", () => { + const specstream = vendorBySlug("specstream"); + expect(specstream).toBeDefined(); + if (!specstream) throw new Error("SpecStream is missing"); + + expect(specstream.mcp.resource).toBe("https://api.specstream.ai/mcp"); + expect(specstream.auth.authorizationServers).toEqual([ + "https://specstream.ai", + ]); + expect(vendorAccess(specstream, "read_only").scopes).not.toContain( + "finds:spend", + ); + expect(vendorAccess(specstream, "read_write").scopes).toContain( + "finds:spend", + ); + expect(specstream.riskyTools).toContain("authorize_capacity_work"); + }); + test("keeps every provider on an HTTPS hosted server", () => { for (const vendor of vendors) { expect(new URL(vendor.mcp.resource).protocol).toBe("https:");