@ratio-mcp-qa/shared

v1.0.8

Published

> The **single source of truth** for the Ratio MCP v2 ecosystem. Shared types, platform constants, and scope data consumed by both the `dev-server` and `docs-server` packages.

Readme

@ratio-mcp-qa/shared

The single source of truth for the Ratio MCP v2 ecosystem. Shared types, platform constants, and scope data consumed by both the dev-server and docs-server packages.


TL;DR

This package is a pure, dependency-free TypeScript library (no runtime deps outside Node's path and os) that eliminates duplication between the two MCP servers in this monorepo. If dev-server and docs-server were microservices, this would be their shared SDK.

Three files. Three jobs.

| File | What it holds | Why it exists | |---|---|---| | constants.ts | Env-driven URLs, store paths, server identity | One variable change retargets the whole stack (QA ↔ prod) | | types.ts | 14 TypeScript interfaces + 1 type alias | Compile-time contracts between writers and readers of the same data | | scopes-data.ts | 147 real scopes across 78 resources + metadata + helpers | Offline anti-hallucination — validate scopes without a network call |


Quick Start

Installing

This package is consumed via pnpm workspace protocol from the monorepo root. You never install it separately.

// From anywhere in dev-server or docs-server
import {
  RATIO_API_BASE_URL,
  ALL_SCOPE_CODES,
  type AppRequirements,
} from '@ratio-mcp-qa/shared';

All exports flow through a single barrel (src/index.ts) so you never reach into individual files.

Building

pnpm build       # compiles src/*.ts to dist/*.js + dist/*.d.ts
pnpm typecheck   # tsc --noEmit

Published artifact is dist/ only (per the files field in package.json).


Why This Package Exists

Three concrete reasons, each one a problem this package solves.

1. One env variable retargets the whole stack

RATIO_API_BASE_URL is read once at module load in constants.ts, then flows to:

  • The developer-dashboard HTTP client (api/developer.ts)
  • The OAuth helpers (api/oauth.ts)
  • The webhook management tools (tools/webhooks.ts)
  • The generated backend's .env file written by scaffold_backend

Change RATIO_API_BASE_URL=https://...prod... in your .env and every one of those targets flips together. No drift between MCP and scaffolded code.

2. Shape contracts between writers and readers

Data shapes like AppRequirements and StoredToken are written by one module (a tool handler) and read by another (a persistent store, or the scaffolded backend). Defining the shape once in shared/ means:

  • Add a field → both consumers see it at compile time.
  • Rename a field → the TypeScript compiler points out every place it lands.
  • No hand-rolled "keep these in sync" comments.

3. Anti-hallucination without a network call

The ALL_SCOPE_CODES Set lets any tool validate a scope code in O(1) — purely offline. Combined with SCOPES_BY_RESOURCE and RESOURCE_METADATA, four tools across two servers can reason about scopes without ever hitting the platform API. If this data lived only on the server, every tool would need HTTP; if it were duplicated per-server, drift would creep in.


Package Layout

packages/shared/
├── src/
│   ├── index.ts          # Barrel re-export of the three files below
│   ├── constants.ts      # 28 lines  — runtime env + paths + identity
│   ├── types.ts          # 183 lines — compile-time data contracts
│   └── scopes-data.ts    # 489 lines — platform truth: 147 scopes × 78 resources
├── dist/                 # Build output (ESM + .d.ts + sourcemaps)
├── package.json
├── tsconfig.json         # extends ../../tsconfig.base.json
└── README.md             # (this file)

Design choices worth knowing:

  • ESM-only ("type": "module" in package.json). Imports use .js extensions even in .ts source — required by Node16 module resolution.
  • Strict TypeScript inherited from tsconfig.base.json (strict: true, full declaration maps for IDE round-tripping).
  • No Zod here. Types are compile-time only; runtime validation lives in the servers. Keeps this package lightweight and server-agnostic.

API Reference

constants.ts

Runtime values. Nine exports in six logical groups.

API URLs (env-driven with QA defaults)

RATIO_API_BASE_URL          // default: https://api-gw-v4.dev.gokwik.in/qa/aes
RATIO_API_VERSION           // 'v1'
RATIO_DEVELOPER_DASHBOARD_URL // default: https://dev-developers.dev.gokwik.in

Read from process.env at module load time. Callers that want env overrides (e.g. dev-server/src/index.ts) must call dotenv.config() before importing from this package.

Persistent store paths

STORE_DIR         // ~/.ratio-v2/
TOKENS_FILE       // ~/.ratio-v2/tokens.json
REQUIREMENTS_FILE // ~/.ratio-v2/requirements.json

Computed with join(homedir(), '.ratio-v2') — cross-platform (mac/linux/windows).

OAuth & developer API

OAUTH_BASE_PATH         // /api/v1/oauth   (interpolated with RATIO_API_VERSION)
DEVELOPER_API_BASE_PATH // /api/developer  (no version — different subsystem)
DEFAULT_REDIRECT_URI    // http://localhost:3000/auth/callback

MCP server identity

DEV_SERVER_NAME    = 'ratio-dev-server'    DEV_SERVER_VERSION  = '1.0.0'
DOCS_SERVER_NAME   = 'ratio-docs-server'   DOCS_SERVER_VERSION = '1.0.0'

Passed to new McpServer({ name, version }) — surfaces in the MCP handshake and in client UIs (Claude Desktop, Claude Code).


types.ts

Compile-time contracts. Erased at build; zero runtime cost.

Scope types (input shape from the platform API)

interface ApiScope {
  id: string;                           // UUID from platform
  code: string;                         // e.g. 'read_orders'
  label: string;                        // 'Read Orders'
  resource: string;                     // 'orders'
  createdAt: string;                    // ISO timestamp
  description?: string;                 // optional enrichment
  use_cases?: string[];
  commonly_paired_with?: string[];
}

interface ResourceMeta {
  description: string;                  // rich text for keyword matching
  codegen_ready: boolean;               // true iff docs-server has a JSON schema
  commonly_needed_with?: string[];
  example_app_types?: string[];
}

interface ScopesApiResponse {
  data: Record<string, ApiScope[]>;     // raw shape of GET /api/v1/scopes
}

The codegen_ready flag is load-bearing: generate_api_routes refuses to emit code for resources where it's false. Today only orders and products are true.

API schema types (docs-server consumption)

interface EndpointParam { name, type, required, description, default?, enum? }
interface EndpointDefinition {
  method, path, controller_order, handler_name,
  required_scope, summary, description?,
  query_params?, path_params?, request_body?,
  response_schema?, extra_headers?
}
interface ResourceSchema {
  resource, base_url, auth?, endpoints, schemas, statuses?
}
interface WebhookEvent { topic, description, resource, required_scope?, example_payload? }
interface WebhooksSchema { version, verification?, retry_policy?, events }
interface OAuthSchema { version, flows }

These match the JSON files in docs-server/src/schemas/ (orders, products, webhooks, oauth). The types give callers type safety without runtime validation overhead.

controller_order is deterministic — codegen emits methods in this stable order so diffs stay clean across regenerations.

Validation types (anti-hallucination)

type ValidationErrorType =
  | 'UNKNOWN_ENDPOINT' | 'WRONG_ORDER' | 'SCOPE_MISMATCH'
  | 'MISSING_ENDPOINT' | 'WRONG_METHOD' | 'WRONG_HANDLER_NAME';

interface ValidationError { type, message, suggestion }
interface ValidationResult { valid, errors, endpoints_validated? }

The LLM consumes these structurally. A stable error union + suggestion field is more valuable than free-form error text — it lets the LLM map errors to fix strategies.

Persistent store shapes

interface AppRequirements {
  app_id: string; app_name?, client_id?, project_path?;
  scopes: string[]; data_needs: string[]; features: string[];
  created_at: string; updated_at: string;
}

interface StoredToken {
  access_token, refresh_token, scopes, merchant_id,
  expires_at, created_at, client_id, client_secret, redirect_uri
}

AppRequirements records live in ~/.ratio-v2/requirements.json. StoredToken records live in ~/.ratio-v2/tokens.json. client_secret is stored in plaintext because this is local developer tooling, not a production vault.

Other helpers

interface ScopeEndpointMapping { scope, resource, endpoints }
interface DeveloperCredentials { developer_token, developer_id, email }
interface ToolResult { [key: string]: unknown }

scopes-data.ts

The data backbone of the entire MCP. Frozen snapshot from GET /api/v1/scopes, last refreshed 2026-04-08.

PRIMARY_RESOURCES — opinionated default ordering

export const PRIMARY_RESOURCES = [
  'orders', 'products', 'customers', 'inventory', 'discounts',
  'fulfillments', 'draft_orders', 'returns', 'shipping', 'gift_cards',
  'analytics', 'channels', 'content', 'files', 'locations',
  'themes', 'markets', 'reports', 'locales', 'publications', 'apps',
] as const;

export type PrimaryResource = (typeof PRIMARY_RESOURCES)[number];

21 resources most apps need. as const gives a readonly tuple so PrimaryResource is a strict union of string literals.

RESOURCE_METADATA — the LLM-readable layer

export const RESOURCE_METADATA: Record<string, ResourceMeta> = {
  orders: {
    description: 'Merchant orders — line items, pricing, customer info, payment and fulfillment status, addresses.',
    codegen_ready: true,
    commonly_needed_with: ['products', 'customers', 'inventory'],
    example_app_types: ['Order analytics', 'Fraud detection', 'Shipping & logistics', 'Accounting'],
  },
  // ... 20 more primary resources
};

Only primary resources have metadata. The other 57 resources in SCOPES_BY_RESOURCE are matched by resource name alone.

The description + example_app_types strings are the fuel for keyword matching in gather_requirements. Rich text here = better matches for developer descriptions like "I want to build a fraud detection tool".

SCOPES_BY_RESOURCE — the scope corpus

export const SCOPES_BY_RESOURCE: Record<string, ApiScope[]> = {
  orders: [
    { code: 'order-cancel',    label: 'Delete Orders', ... },
    { code: 'order-cancelled', label: 'Order cancel', ... },
    { code: 'order-create',    label: 'Order Create', ... },
    { code: 'order-update',    label: 'Order Update', ... },
    { code: 'read_orders',     label: 'Read Orders', ... },
    { code: 'write_orders',    label: 'Write Orders', ... },
  ],
  customers: [
    { code: 'manage_customers', ... },
    { code: 'read_customers', ... },
    { code: 'write_customers', ... },
  ],
  // ... 76 more resources
};

All 78 resources, 147 scopes. Naming conventions are inconsistent — most follow read_X / write_X, but watch out for:

  • orders uses kebab-case legacy codes: order-create, order-update, order-cancel, order-cancelled
  • customers has a manage_customers code
  • theme_code only has write_theme_code (no read)
  • Some resources are read-only, some write-only, some both

Any code that infers scope relationships from names must handle these exceptions. getResourceForScope() already does.

ALL_SCOPE_CODES — the anti-hallucination backstop

export const ALL_SCOPE_CODES: Set<string> = new Set(
  Object.values(SCOPES_BY_RESOURCE).flatMap((scopes) => scopes.map((s) => s.code))
);

Built once at module load. O(1) membership check thereafter. If the LLM fabricates read_store_analytics, it's rejected instantly — no network call.

Helper functions

getResourceForScope(code: string): string | undefined
getAllResources(): string[]   // all 78 resource names

getResourceForScope handles all naming variants — kebab-case and manage_ exceptions included. Prefer it over hand-rolling code.split('_')[1].


Who Uses What

The real-world consumer map. 17 files across the two servers import from this package.

Constants (8 files)

| Import | Consumer | Purpose | |---|---|---| | RATIO_API_BASE_URL | api/developer, api/oauth, tools/webhooks, tools/scaffold | All HTTP calls; injected into generated backend .env | | OAUTH_BASE_PATH | api/oauth | Composed with base URL for OAuth endpoints | | STORE_DIR, TOKENS_FILE | store/token-store | Persistent token cache paths | | STORE_DIR, REQUIREMENTS_FILE | store/requirements-store | Persistent requirements store paths | | DEV_SERVER_NAME/VERSION | dev-server/server, orchestrated-server | MCP handshake identity | | DOCS_SERVER_NAME/VERSION | docs-server/server | MCP handshake identity |

Types (9 files)

| Import | Consumer | Purpose | |---|---|---| | AppRequirements | store/requirements-store, tools/requirements | Shape of persisted app state; writer + reader agree | | StoredToken | store/token-store, tools/auth | Shape of cached tokens; writer + .env injector agree | | ResourceSchema, WebhooksSchema, OAuthSchema | docs-server/utils/schema-loader | Typed JSON loading — no runtime cost | | ValidationResult, ValidationError | docs-server/tools/validate-code | Stable error contract for the LLM | | ScopeEndpointMapping | docs-server/tools/get-scope-map | Codegen handoff shape |

Scope data (4 files, all the "scope reasoning" tools)

| Import | Consumer | Purpose | |---|---|---| | ALL_SCOPE_CODES | tools/requirements | O(1) offline validation in define_app_requirements + validate_scopes | | SCOPES_BY_RESOURCE | tools/requirements, tools/gather-requirements, docs-server/tools/get-scopes, lookup-docs | Scope lookups, label resolution, pairing suggestions | | RESOURCE_METADATA | tools/requirements, tools/gather-requirements, docs-server/tools/get-scopes, lookup-docs | Keyword matching fuel, codegen-ready flag, browse enrichment | | PRIMARY_RESOURCES | docs-server/tools/get-scopes, lookup-docs | Sort primary-first in scope browsing | | getResourceForScope() | tools/requirements | Derive data_needs from scope list; group scopes by resource | | getAllResources() | docs-server/tools/get-scopes | Full 78-resource enumeration |


Usage Examples

1. Validating scopes before presenting to the developer

From dev-server/src/tools/requirements.ts:

import { ALL_SCOPE_CODES } from '@ratio-mcp-qa/shared';

const validScopes: string[] = [];
const invalidScopes: string[] = [];
for (const scope of scopes) {
  if (ALL_SCOPE_CODES.has(scope)) {
    validScopes.push(scope);
  } else {
    invalidScopes.push(scope);
  }
}

This is how validate_scopes and define_app_requirements catch hallucinated scopes before they reach the developer.

2. Typed HTTP client targeting

From dev-server/src/api/developer.ts:

import { RATIO_API_BASE_URL } from '@ratio-mcp-qa/shared';

// Every tool that calls the platform hits this URL
const url = `${RATIO_API_BASE_URL}/api/developer${endpoint}`;

The same RATIO_API_BASE_URL is written into the scaffolded backend's .env, so MCP and generated code agree on the target environment.

3. Keyword matching for developer descriptions

From dev-server/src/tools/gather-requirements.ts:

import { RESOURCE_METADATA, SCOPES_BY_RESOURCE } from '@ratio-mcp-qa/shared';

for (const [resource, meta] of Object.entries(RESOURCE_METADATA)) {
  const metaText = [meta.description, ...(meta.example_app_types || [])]
    .join(' ').toLowerCase();
  // score resource by token overlap with metaText
}

The richer the metadata strings, the better the matches.

4. Persistent store with shared shape

From dev-server/src/store/requirements-store.ts:

import { STORE_DIR, REQUIREMENTS_FILE } from '@ratio-mcp-qa/shared';
import type { AppRequirements } from '@ratio-mcp-qa/shared';

interface RequirementsStoreFile {
  version: 1;
  requirements: Record<string, AppRequirements>;
}

The shape defined in shared/ is used both by the store (reader) and by tools/requirements.ts (writer). No hand-rolled sync.


Gotchas & Conventions

Env var timing

constants.ts reads process.env at module load time. If a consumer imports @ratio-mcp-qa/shared before calling dotenv.config(), the defaults are locked in. dev-server/src/index.ts calls dotenv.config() as its very first line for this reason.

ESM .js imports on .ts sources

export * from './types.js';  // yes, .js — even though the file is types.ts

Required by Node16 module resolution. TypeScript rewrites the paths at build. A common trap when adding new files to this package.

Scope data is frozen

scopes-data.ts is a snapshot, not a live fetch. To refresh:

  1. Hit GET /api/v1/scopes on the platform
  2. Regenerate SCOPES_BY_RESOURCE (mirror the exact shape)
  3. Update the Last updated comment at the top of the file
  4. Bump the package version
  5. Rebuild consumers (pnpm build)

There's no automatic drift detection — stale data here means stale data everywhere.

Only 2 codegen-ready resources today

orders and products. Every other resource has codegen_ready: false. generate_api_routes checks this flag and silently skips non-ready resources. This is a hidden product constraint that lives in RESOURCE_METADATA.

Naming inconsistencies in scopes

Don't assume read_X / write_X. Always use getResourceForScope() instead of splitting on _.

ALL_SCOPE_CODES is a singleton

Built once at load. Safe to share across all consumers. Never mutate it — it's a Set, but treat it as frozen.

Add a field to a type → rebuild is required

Consumers import from dist/, not src/. After editing types.ts or constants.ts, run pnpm build in packages/shared before expecting consumers to see the change.


Build & Development

Building

pnpm build       # tsc → dist/*.js + *.d.ts + sourcemaps
pnpm typecheck   # tsc --noEmit (CI-friendly)

Output

dist/ contains:

  • index.js, types.js, constants.js, scopes-data.js — ESM modules
  • *.d.ts — type declarations
  • *.d.ts.map + *.js.map — full source navigation from consumers

Published artifact

The files field in package.json ships only dist/. The exports map points ESM consumers at dist/index.js and types at dist/index.d.ts.

Inherited config

tsconfig.json extends ../../tsconfig.base.json:

  • target: ES2022, module: Node16, moduleResolution: Node16
  • strict: true, declaration: true, full sourcemaps
  • outDir: dist, rootDir: src

File Quick Reference

| File | Lines | Exports | Runtime? | |---|---|---|---| | index.ts | 3 | Barrel re-export | No | | constants.ts | 28 | 10 constants | Yes (reads process.env at load) | | types.ts | 183 | 14 interfaces + 1 type alias | No (erased at compile) | | scopes-data.ts | 489 | 5 exports + 2 functions + 1 type | Yes (builds Set at load) |

~703 lines of pure, dependency-free TypeScript underpinning the entire 40-tool MCP ecosystem.


Related Packages

  • @ratio-mcp-qa/dev-server — The main tool server (33 tools). Consumes constants, types, and all of scopes-data.
  • @ratio-mcp-qa/docs-server — Read-only reference server (7 tools). Consumes constants, schema types, and all of scopes-data.

See the monorepo root CLAUDE.md for the full architecture overview.