Skip to content

feat: configurable default market for location/language fallbacks#72

Open
bookingseo wants to merge 3 commits into
every-app:mainfrom
bookingseo:feat/configurable-default-market
Open

feat: configurable default market for location/language fallbacks#72
bookingseo wants to merge 3 commits into
every-app:mainfrom
bookingseo:feat/configurable-default-market

Conversation

@bookingseo

@bookingseo bookingseo commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Why

Every location/language fallback is hardcoded to the United States (2840/en). A self-hosted deployment serving another market has to pass locationCode/languageCode on every single MCP call and re-pick the location in the UI on every browser. We run OpenSEO for the Vietnamese market and hit this constantly — but the change is generic and benefits any non-US self-hoster.

What

Two optional env vars make the default market configurable; nothing changes unless they are set, and explicit args always win — the default only changes what omitted means.

  • src/server/lib/market-defaults.ts (new): getDefaultMarket() reads OPENSEO_DEFAULT_LOCATION_CODE / OPENSEO_DEFAULT_LANGUAGE_CODE. The language auto-derives from the location's native language when unset (e.g. 2704vi). Invalid values log a warning and fall back to the US — a typo can't take calls down. Memoized; one openseo_default_market log line at startup.
  • The 8 MCP tools that defaulted these args now resolve through it: get_serp_results, research_keywords, save_keywords, get_domain_overview, get_domain_keyword_suggestions, get_keyword_metrics, get_local_serp_results, get_google_business_questions. The Labs/language asserts run against the resolved pair, which also fixes a latent edge: an explicit languageCode with an omitted location used to validate against the hardcoded US location.
  • get_ranked_keywords / find_serp_competitors (Labs-only, market object): an omitted market follows the default only when it is Labs-served — an Ads-data default like Iceland keeps them on the US instead of producing charged failures. The explicit selector still only accepts the US; extending it per-country is left out of scope here.
  • whoami reports the active defaultMarket {locationCode, languageCode, label} (additive, optional field) so MCP clients can discover the deployment's default.
  • Web UI: the initial keyword location honors VITE_DEFAULT_LOCATION_CODE (inlined at build — works with Dockerfile.selfhost's runtime build via the new compose passthrough). The existing per-browser localStorage preference still wins.
  • compose.yaml passes the three vars through; .env.example documents them.

Testing

  • 10 new tests: market-defaults.test.ts (env parsing, native-language derivation, unserved-pair fallback, unsupported-code fallback, empty-string handling, memoization) and dataforseo-research-tools.market.test.ts (explicit US wins under a non-US default; omitted market follows a Labs default; Ads-provider default falls back to the US).
  • Full suite green: 587 tests, pnpm ci:check clean.
  • Verified live on our self-hosted deployment with OPENSEO_DEFAULT_LOCATION_CODE=2704: whoami reports Vietnam, and get_serp_results without a locationCode returns Vietnamese-market SERPs.

Notes

  • MCP tool schema descriptions now say "defaults to the server's default market (check whoami)" instead of hardcoding 2840.
  • DEFAULT_LANGUAGE_CODE in src/server/mcp/schemas.ts lost its last consumer and is removed; DEFAULT_LOCATION_CODE stays (US fallback anchor).

Greptile Summary

This PR adds configurable project default markets for keyword and SERP workflows. The main changes are:

  • Project create and settings flows now persist locationCode and languageCode defaults.
  • MCP project auth now exposes the authorized project market to tool handlers.
  • Keyword, SERP, domain, and saved-keyword tools now resolve omitted market inputs against project defaults.
  • list_projects now returns market fields so MCP clients can discover fallback behavior.
  • Tests were updated and added for project market validation and MCP market fallback behavior.

Confidence Score: 5/5

Safe to merge with low risk.

The changes follow the existing serverFunction, service, and repository boundaries. Market inputs are validated with zod and resolved before provider calls. Targeted tests cover market derivation, validation, and MCP fallback behavior. No blocking correctness or security issues were identified.

No files require special attention.

T-Rex T-Rex Logs

What T-Rex did

  • Executed the market defaults tests and confirmed all tests passed; 3 test files passed, 21 tests in total, MARKET_DEFAULTS_TEST_PRESENT: no, and EXIT_CODE: 0.
  • Ran the typecheck step and verified it completed with EXIT_CODE: 0.
  • Noted that an earlier shell-wrapper attempt failed due to /bin/sh not supporting PIPESTATUS, and that a corrected Bash-wrapped run is the authoritative passing artifact.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
src/server/features/projects/services/projects.ts Resolves and validates project market input before repository writes, including language-only updates.
src/server/mcp/project-auth.ts Exposes authorized project context to MCP handlers and centralizes request market fallback resolution.
src/server/mcp/tools/dataforseo-research-tools.ts Routes Labs and keyword metric tools through project-aware market resolution while retaining US fallback for Ads-only defaults on Labs-only calls.
src/server/mcp/tools/get-serp-results.ts Applies project default market resolution for each SERP query that omits location or language.
src/client/features/projects/ProjectSettings.tsx Adds editable project market fields with language options derived from the selected location.
src/types/schemas/projects.ts Extends project create/update schemas with zod-validated optional market fields.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
  participant Client as Web UI / MCP Client
  participant Auth as withMcpProjectAuth
  participant Project as ProjectService/Repository
  participant Resolve as resolveRequestMarket / resolveMarket
  participant Tool as MCP Tool Handler
  participant DFS as DataForSEO/Domain/Keyword Service

  Client->>Auth: call tool with projectId and optional market args
  Auth->>Project: authorize project and load project defaults
  Project-->>Auth: "project {locationCode, languageCode}"
  Auth->>Tool: handler args + project context
  Tool->>Resolve: combine explicit args with project defaults
  Resolve-->>Tool: resolved location/language pair
  Tool->>DFS: provider request using resolved market
  DFS-->>Tool: market-specific results
  Tool-->>Client: MCP response + project meta
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
  participant Client as Web UI / MCP Client
  participant Auth as withMcpProjectAuth
  participant Project as ProjectService/Repository
  participant Resolve as resolveRequestMarket / resolveMarket
  participant Tool as MCP Tool Handler
  participant DFS as DataForSEO/Domain/Keyword Service

  Client->>Auth: call tool with projectId and optional market args
  Auth->>Project: authorize project and load project defaults
  Project-->>Auth: "project {locationCode, languageCode}"
  Auth->>Tool: handler args + project context
  Tool->>Resolve: combine explicit args with project defaults
  Resolve-->>Tool: resolved location/language pair
  Tool->>DFS: provider request using resolved market
  DFS-->>Tool: market-specific results
  Tool-->>Client: MCP response + project meta
Loading

Reviews (1): Last reviewed commit: "fix: resolve market overrides as a pair,..." | Re-trigger Greptile

@bensenescu

Copy link
Copy Markdown
Contributor

What do you think about setting these at a project level and storing your choice in the database instead? We could also have env vars, but I think that's more confusing for most people.

@bookingseo

Copy link
Copy Markdown
Contributor Author

Agreed — project-level in the DB is the better home for this. Env vars were the smallest-diff approach, but a per-project default is more discoverable, works for orgs with projects in different markets, and doesn't require touching server config at all. Happy to drop the env vars entirely.

Concrete shape I'd rework this PR into:

  • Schema: nullable default_location_code (int) + default_language_code (text) on projects, both dialects (src/db/app.schema.ts + src/db/pg/app.schema.ts) + migrations. NULL = today's behavior (US/en), so no backfill and zero change for existing projects.
  • Setting it: a market picker in CreateProjectModal (defaulting to US) and editable in ProjectSettings. Language auto-derives from the location's native language unless overridden, same as the current PR.
  • MCP resolution: explicit locationCode/languageCode args still always win → then the project's default → then US. Every affected tool already resolves the project through withMcpProjectAuth, so the row is at hand — no extra query.
  • Discovery: expose defaultMarket on list_projects rows (instead of the whoami field in the current PR) so MCP agents see the right default per project.
  • Web UI: keyword/domain location pickers seed from the project's default. Question: keep the existing per-browser localStorage "last used location" as an override on top, or drop it in favor of the project setting? I'd lean keep (it's harmless and sticky per user), but either is a small change.
  • The Labs-only guard from the current PR stays: get_ranked_keywords/find_serp_competitors only follow a project default that Labs serves, otherwise stay on US.

If that shape works for you I'll rework this PR (same branch). Also fine to split schema+MCP from the UI pieces if you'd rather review them separately.

@bookingseo

Copy link
Copy Markdown
Contributor Author

Reworked to per-project as suggested — pushed as a second commit (3d69a11) so the pivot is easy to review; happy to squash.

Turns out it's even simpler than I proposed: projects already carries locationCode/languageCode ("set during onboarding and reused by every project-scoped data call") — so there's no schema change and no env vars at all. The commit just makes the rest of the product honor those columns:

  • withMcpProjectAuth now exposes the project row it already fetches for the auth gate, and the 8 defaulting MCP tools fall back to project.locationCode/languageCode — zero extra queries.
  • list_projects returns each project's market (the whoami field from the first commit is reverted — an org-level tool has no single project to speak for).
  • CreateProjectModal gets a default-market picker (language auto-derives from the location's native language); ProjectSettings edits both, with the language list constrained to what DataForSEO serves for that location. The keyword-research picker seeds from the project default; the existing per-browser preference still wins once the user picks one.
  • createProject/updateProject accept optional locationCode/languageCode; a language-only update is validated against the stored location, so an unserved pair is rejected for free instead of surfacing later as a charged DataForSEO error.
  • The Labs-only market tools (get_ranked_keywords/find_serp_competitors) follow the project default only when Labs serves it — an Ads-data market (e.g. Iceland) falls back to the US instead of breaking them.

Full suite green (583 tests, +6 new for the service validation and market resolution); ci:check clean. I kept the localStorage "last used location" as an override on top of the project default per my earlier lean — say the word if you'd rather drop it.

@bookingseo

Copy link
Copy Markdown
Contributor Author

Pushed 2f8bf23 with two fixes from self-review:

  • Market overrides now resolve as a pair. Previously a tool call overriding only locationCode kept the project's stored languageCode, so a Vietnam-default project querying Germany sent languageCode: 'vi' to a paid endpoint (or tripped the language validation blaming a language the caller never set). A shared resolveRequestMarket helper snaps the language to the overridden location's default; all six tools use it.
  • Language-only project updates write only the language column. updateProject no longer folds the pre-read location back into the write, so two concurrent partial market updates can't silently overwrite each other's half; a location-only change resolves entirely from its input and skips the extra read.

589 tests green, ci:check clean.

Every location/language fallback was hardcoded to the United States
(2840/en), which makes self-hosted deployments outside the US pass
locationCode/languageCode on every single call. getDefaultMarket()
reads OPENSEO_DEFAULT_LOCATION_CODE / OPENSEO_DEFAULT_LANGUAGE_CODE
(language auto-derives from the location's native language; invalid
values warn and fall back to the US so a typo can't break calls) and
feeds the eight MCP tools that default these args. whoami reports the
active defaultMarket so agents can discover it. The web UI's initial
location honors VITE_DEFAULT_LOCATION_CODE; the per-browser preference
still wins. Explicit args always win — the default only changes what
omitted means.

The Labs-only market tools (get_ranked_keywords, find_serp_competitors)
follow the default when the market object is omitted, guarded so an
Ads-provider default (e.g. Iceland) keeps them on the US instead of
breaking.
Per review: the projects table already carries locationCode/languageCode
('set during onboarding and reused by every project-scoped data call'),
so the default market belongs there — no schema change, no env vars.

- withMcpProjectAuth exposes the project row it already fetches for the
  auth gate; the eight defaulting tools fall back to
  project.locationCode/languageCode (zero extra queries).
- list_projects returns each project's locationCode/languageCode (the
  whoami field from the previous commit is reverted — an org-level tool
  has no single project to speak for).
- CreateProjectModal gains a default-market picker (language derives
  from the location's native language); ProjectSettings edits both, with
  the language list constrained to what DataForSEO serves for the
  location. The keyword picker seeds from the project default; the
  per-browser preference still wins once set.
- createProject/updateProject accept optional locationCode/languageCode;
  a language-only update is validated against the stored location so an
  unserved pair is rejected free instead of failing as a charged
  provider error.
- The Labs-only market tools follow the project default only when Labs
  serves it (an Ads-data market like Iceland falls back to the US).
…n on language-only updates

A tool call overriding only the location was paired with the project's
stored language — a Vietnam project querying Germany sent languageCode
'vi' to a paid endpoint. resolveRequestMarket snaps the language to the
overridden location's default instead, shared across all six tools.

updateProject no longer folds a pre-read location back into the write:
a language-only change updates only the language column, so concurrent
partial market updates can't clobber each other's half; a location
change resolves entirely from its input and skips the read.
@bookingseo bookingseo force-pushed the feat/configurable-default-market branch from 2f8bf23 to 1d6203e Compare July 10, 2026 08:11
@bensenescu

Copy link
Copy Markdown
Contributor

@greptileai

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants