69 Commits

Author SHA1 Message Date
Ben Senescu
ad2d28ea6f
fix(ga4): honor full date ranges instead of clamping to 90 days (#563) 2026-08-28 16:15:22 -04:00
Ben Senescu
749b38118d
fix: show GA4 MCP rows in Claude text output (#553) 2026-08-27 18:02:13 -04:00
Ben Senescu
ea162a4391
feat(orgs): multi-user workspaces — roles, invitations, membership enforcement (#473) 2026-08-26 16:54:08 -04:00
Ben Senescu
61b32ec647
fix: extend OAuth state cookie lifetime to match the 10-minute state TTL (#544) 2026-08-26 14:37:46 -04:00
Ben Senescu
9b12e073a8
fix: replace the broken per-key API rate limit with a real 5000/min per-user /mcp limit (#545) 2026-08-26 12:09:31 -04:00
Ben Senescu
824e914eb1
fix(mcp): refuse subscriptions/listen streams — MCP serving is stateless (EVE-95) (#548) 2026-08-26 11:48:20 -04:00
Ben Senescu
022a7f1944
SERP analysis: depth 20 by default, load top 100 on demand; classify DataForSEO timeouts (#523) 2026-08-26 11:17:18 -04:00
Ben Senescu
a1c6eb6b11
fix: meta-only-response test used toSorted, banned by the ES2022 lib floor (#537) 2026-08-26 10:37:50 -04:00
Ben Senescu
0311e72a96
PostHog reports every server error as affecting as many users as it has events (#534) 2026-08-26 10:23:03 -04:00
Ben Senescu
67d68281e3
fix(mcp): accept expected partial outputs (#449) 2026-08-26 10:04:40 -04:00
Ben Senescu
b631715112
Blank page on older browsers: replace ES2023 array methods with Remeda (#535) 2026-08-26 10:02:03 -04:00
Ben Senescu
8db0dd3246
First site audit over MCP fails with a raw billing error (#525) 2026-08-26 09:59:35 -04:00
Ben Senescu
b592dc5043
fix(mcp): classify project input validation (#455) 2026-08-26 09:39:09 -04:00
Ben Senescu
6e02186a2b Add ChatGPT plugin submission manifest (#519) 2026-08-23 18:18:17 -04:00
Ben Senescu
4ba2d6c175 Require exact hosted MCP origins (#515)
* Require exact hosted MCP origins

* Clarify exact MCP origin policy
2026-08-23 18:18:17 -04:00
Ben Senescu
6470b875ad Allow SurfMind Chrome extension MCP origin (#513) 2026-08-23 18:18:17 -04:00
Ben Senescu
43265048df
Tame audit polling: SAM's status tool waits server-side; Lighthouse becomes opt-in for agents (#510) 2026-08-19 14:22:33 -04:00
Ben Senescu
a6f96c516e
Remove GA4 launch gating; dashboard shows an Organic traffic card once connected (#505) 2026-08-19 13:19:55 -04:00
Ben Senescu
a5a953e478
Project memory + SAM skills: shared per-project AI context (spec 0010) (#493) 2026-08-19 09:46:07 -04:00
Ben Senescu
cfa5bc2cf5
Gate GA4 MCP tools per-user; fix false ga4_malformed_response on empty comparison periods (#497) 2026-08-18 15:56:38 -04:00
Ben Senescu
7738f72333
Local SEO MCP tools: business profile, reviews, updates, categories, rank grid (#489) 2026-08-17 23:21:34 -04:00
Ben Senescu
c8b3eb9b0a
Add exact URL / subfolder / domain / subdomain research scopes (EVE-56) (#487) 2026-08-14 21:52:32 -04:00
Ben Senescu
db45a0dd6d
Fix MCP saved keyword metrics (#483) 2026-08-13 20:16:20 -04:00
Ben Senescu
72eaa09c32
Hide Analytics section when GA4 OAuth app is pending (#475) 2026-08-12 19:57:29 -04:00
Ben Senescu
84e8d0be99
Serve legacy MCP JSON requests statelessly to stop per-request server retention (#478) 2026-08-11 20:15:48 -04:00
Ben Senescu
16eb599270
MCP API key support (hosted mode) (#438) 2026-08-11 19:20:44 -04:00
Ben Senescu
cfc8456767
feat(mcp): migrate to MCP SDK v2 stateless handler (#464) 2026-08-08 12:44:48 -04:00
Ben Senescu
e2c84803f2
feat: add GA4 MCP insights and rank tracking management (#461) 2026-08-07 13:47:18 -04:00
Ben Senescu
8a728563d4
fix(mcp): stop breaking public OAuth clients at token refresh (#420)
* fix(mcp): stop breaking public OAuth clients at token refresh

The DCR shim force-upgraded every public client (token_endpoint_auth_method
"none" or omitted) to client_secret_post so Perplexity would accept the
registration response. That made the stored client confidential, so the
token endpoint demanded client authentication on every grant — and MCP
clients that discard the secret (Codex) lost their session at first token
expiry with "invalid_client: missing client_secret".

Register those clients as true public clients instead (PKCE + the
provider's grant-to-client binding secure that flow), and satisfy
Perplexity by decorating only the registration response with a placeholder
client_secret and client_secret_post. The provider skips secret validation
for public clients, so clients that send the placeholder and clients that
never store it both keep working, including on refresh.

* refactor(mcp): only rebuild DCR requests that actually change

* fix(mcp): satisfy type-aware lint in DCR shims

oxlint --type-aware rejected the Record<string, unknown> assertions used to
read untrusted DCR payloads. Parse both with loose Zod schemas instead, per
the repo's trust-boundary convention, which also replaces the hand-rolled
object guards.
2026-08-05 16:10:28 -04:00
Ben Senescu
1e8a924c4c
Site Audit - Improve reliability and performance with Durable Objects 2026-07-30 00:51:50 -04:00
Amine Benboubker
b0b5528a3a
feat(mcp): add create_project tool (#114) 2026-07-23 10:07:02 -04:00
Slim Bouras
2c153f9a20
feat(mcp): accept locationCode/languageCode on Labs research tools (#113)
Co-authored-by: isatimur <isatimur@users.noreply.github.com>
2026-07-23 10:05:43 -04:00
Ben Senescu
eda5e1e354
Project dashboard: onboarding checklist + domain overview (#398) 2026-07-18 21:52:35 -04:00
Ben Senescu
1398605ddc
feat: anonymous self-host telemetry heartbeat (#395) 2026-07-18 20:07:42 -04:00
bookingseo
0f08437cd0
feat: configurable default market for location/language fallbacks (#72) 2026-07-16 10:46:49 -04:00
Ben Senescu
dae0067233
feat(gsc): support multiple Google accounts per user (#373) 2026-07-09 21:49:08 -04:00
Ben Senescu
f2c41b1db1 MCP activation + usage PostHog events, North Star dashboard prompt (#369)
* Add MCP activation + usage PostHog events and North Star dashboard prompt

- mcp:authorize_success (server) on OAuth consent completion
- mcp:tool_call (server) on every MCP tool invocation, with tool/success/
  error_code/client_id and mcp_client vs in_app_agent source
- mcp:consent_viewed / mcp:consent_denied on the OAuth consent page
- mcp:setup_url_copy / mcp:setup_command_copy intent events on /ai
- docs/posthog-north-star-dashboard.md: prompt to configure the PostHog
  core-actions dashboard (activation rate, MCP setup funnel, daily MCP users)

* Fix codex review findings: count schema-rejected MCP calls as failures, wrap audit tools

- mcp:tool_call now fires after output validation; schema mismatches report
  success:false with MCP_OUTPUT_VALIDATION (the SDK surfaces them to the
  client as JSON-RPC errors)
- run_site_audit/get_audit_status/get_audit_issues/get_audit_pages were
  registered without instrumentMcpToolHandler, so their usage and failures
  were invisible

* Remove dashboard prompt doc
2026-07-07 22:08:14 -04:00
Ben Senescu
1c74fded7b Site audit P0 (1/3): issue engine, incremental persistence, block detection (#362)
* Site audit P0 (1/3): issue engine, incremental persistence, block detection

Server-side foundation of the P0 feature set from docs/site-audit-pm-research.md:

- Issue engine: shared registry of issue types (severity, explanation,
  how-to-fix). Per-page reporters run inside crawl steps; cross-page checks
  (duplicate titles/descriptions/content, broken internal links, redirect
  chains/loops, orphan pages) run at finalize as SQL over the persisted crawl.
- New audit_links + audit_issues tables, audit_pages columns (depth, content
  hash, header signals, fetch class, sitemap flag); audit tables moved to
  src/db/{,pg/}audit.schema.ts; migrations 0029 (D1) / 0006 (PG).
- Incremental persistence: pages/links/issues written inside each crawl-batch
  step with deterministic row ids + upserts (retry idempotent); slim step
  state; robots.txt checkpointed as step state; merged progress steps keep a
  10k-page crawl within the Workflows step budget.
- Crawler: manual redirect handling with inline follow of normalization-
  equivalent redirects, response header capture (X-Robots-Tag, Link
  rel=canonical), BFS depth, sitemap-last seeding, SSRF check on discovered
  links, honest 'we were blocked' classification (403/429/cf-mitigated/
  challenge).
- MCP: run_site_audit, get_audit_status, get_audit_issues, get_audit_pages;
  limitTier resolved via shared AuditService.resolveAuditLimitTier.
- Lighthouse strategies reduced to auto/none (legacy all/manual map on read).
- Self-healing: getStatus reconciles audits whose workflow instance errored/
  terminated without reaching mark-failed.

The Issues UI and the badseo.dev e2e fixture site stack on top of this PR.

Deploy notes: run db:migrate:prod (additive); terminate running audits before
deploying — the workflow step structure changed and in-flight instances cannot
replay under the new code (a finalize guard fails them loudly instead of
completing empty).

* Store only internal link edges in audit_links

Both consumers (broken-internal-link and orphan checks) filter on
isInternal; per-page external counts already live on audit_pages.
Dropping external rows cuts stored edges on outbound-heavy sites.
Column stays so P1 external-link checks can re-add rows without a
migration.

* Review fixes: failAudit CAS guard, dedupe hash helpers, cheaper checks

- failAudit only transitions running audits, so the getStatus reconciler
  can't flip a just-completed audit to failed when it races finalize
- collapse the duplicate SHA-256 helper into audit/ids.ts
- finalize integrity guard uses a limit-1 existence probe instead of
  fetching every page row
- get_audit_status MCP tool no longer reads the audit row twice when an
  explicit auditId is given
2026-07-07 22:08:14 -04:00
Ben Senescu
c9bcc444b4
Triage unresolved PostHog errors: DataForSEO validation, DO retry, Autumn 429, exception noise (#333) 2026-07-05 22:38:53 -04:00
Ben Senescu
41d37792e7
feat: SAM — in-app SEO agent with full MCP toolset (#322) 2026-07-03 18:48:18 -04:00
Ben Senescu
4f17fe4942 Triage production log errors: audit crash, Autumn webhook FK, PostHog capture, auth rate-limit IP, log noise (#327) 2026-07-02 18:15:46 -04:00
Ben Senescu
819e33be07 fix(mcp): 405 the standalone GET SSE stream to stop /mcp OOM (#325)
The stateless MCP server returns JSON on POST (enableJsonResponse) and
pushes no server-initiated messages, so the optional standalone GET SSE
stream serves no purpose. Left enabled, each GET holds an SSE stream open
indefinitely (25s keepalive, no eventStore) and pins a fresh per-request
McpServer (~5MB of tools + Zod schemas); a few dozen concurrent connected
clients exceed the 128MB isolate limit. This was 100% of the /mcp
exceededMemory OOMs (GET only; POST never OOMed).

Return 405 (spec-compliant 'no standalone stream') before building the
server, so GET allocates nothing. Also removes the bulk of the elevated
GET canceled / responseStreamDisconnected outcomes.
2026-07-02 18:15:46 -04:00
Ben Senescu
4e48f8344f Fix production errors: onboarding crash hardening + DataForSEO spend/noise cleanup (#282) 2026-07-02 18:15:46 -04:00
Ben Senescu
868924e1fe refactor(keywords): unify keyword-metric fetching behind one helper (#320) 2026-07-02 18:15:46 -04:00
Ben Senescu
b733be4a5e
release: v0.0.23 - fix mcp for pi + refresh saved keyword metrics (#56)
* fix(mcp): render full row data in tool text output, not just counts

MCP clients that surface only the text content block (not structuredContent)
saw count/truncated summaries instead of the actual rows. Add a shared table
renderer (server/mcp/table.ts) and use it so every row ships in the text block
across research_keywords, get_ranked_keywords, get_keyword_metrics,
search_local_businesses, get_local_serp_results, get_google_business_questions,
find_serp_competitors, get_backlinks_profile, get_backlinks_overview,
get_domain_keyword_suggestions, get_rank_tracker, get_serp_results, and
get_search_console_performance.

* test(mcp): assert tool text output renders full row tables

Cover the shared table renderer (table.test.ts) and add per-tool assertions
that each tool's text content block contains the actual row data (exact
rendered rows, null->em-dash), guarding against a column wired to the wrong
field. Adds tool-text-output.test.ts for the service-backed tools and text
assertions to the DataForSEO and Search Console tool tests.

* release: v0.0.23

* style(mcp): prettier-format tool table columns
2026-07-02 13:01:44 -04:00
Matt Van Horn
1dabbc88a6
feat: add paginated get_backlinks_profile MCP tool (#38)
* feat: add paginated get_backlinks_profile MCP tool

Exposes detailed per-link backlink rows over MCP, reusing BacklinksService
paginated row fetching. Closes #36.

* fix(test): use type-narrowing text assertion to satisfy oxlint

expect.stringContaining inside toMatchObject tripped
typescript-eslint(no-unsafe-assignment), breaking ci:check. Match the
.toContain() pattern used elsewhere in the MCP tool tests.

* refactor(mcp): simplify get_backlinks_profile handler + fix cost estimate

- Drop the redundant backlinksRowsPageRequestSchema.parse re-validation in
  the handler; the MCP SDK already validates args against inputSchema. Build
  the service request straight from args, removing the duplicated defaults
  that could silently diverge.
- Preserve the target length cap by adding .max(2048) to the input schema
  (previously enforced only via the re-parse).
- Correct the credit estimate in the tool description: measured cost is
  ~30 credits/page, not the ~200-500 copied from get_backlinks_overview.
- Minor: type the statuses accumulator.

* fix(mcp): correct get_backlinks_overview credit estimate

Measured real cost: summary (~26 credits) + history for domain scope
(~29 credits) = ~50 per domain, ~25 for a single page. The previous
~200-500 figure was a ~5-10x overestimate (same one get_backlinks_profile
inherited).

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Ben Senescu <bensenescu@gmail.com>
2026-06-28 18:47:24 -04:00
Ben Senescu
d339f0c3d9
Fix MCP output validation and error reporting 2026-06-16 13:52:59 -04:00
Ben Senescu
9abed9278f
Remove free trial: subscription paywall + 30-day money-back guarantee (#251) 2026-06-14 21:21:04 -04:00
Ben Senescu
b809f87b5e
Hybrid keyword data: Google Ads coverage for non-Labs countries + opt-in clickstream volumes (#262) 2026-06-12 14:08:11 -04:00
Ben Senescu
e41b52ca56
MCP directory metadata, resources/prompts handlers, full param descriptions (#260) 2026-06-11 16:11:24 -04:00