SCG International

Settings

API & MCP reference

Consumer-facing reference for the REST API at /api/v1/* and the MCP server at http://<host>:4002/mcp. Both share one auth model: per-consumer Bearer tokens issued at /settings/tokens. Every request is recorded in activity log.

Authentication

All /api/v1/* and MCP requests require Authorization: Bearer <token>. Tokens are evs_…-prefixed strings issued at /settings/tokens. The Web UI itself (/vehicles, /ingest, /settings/*) uses Auth.js + Microsoft Entra ID SSO — Bearer tokens are NOT used there.

Token storage is plaintext (deliberate, for an internal tool — see schema comment in db/schema.sql). The settings UI shows a masked preview evs_xxxx…xxxx with a copy button. Revocation is soft; the original label is preserved for audit. Labels are unique among active tokens — revoke first if you want to re-issue the same label.

bash# Issue a token from the UI: http://<host>:3000/settings/tokens
# Then use it as Bearer for every API + MCP request:
TOK="evs_YOUR_TOKEN_HERE"
curl -H "Authorization: Bearer $TOK" http://<host>:3000/api/v1/categories

REST API

JSON over HTTP. Base path /api/v1. Bilingual (EN/TH) — see Notes.

GET/api/v1/categories

List vehicle categories that have at least one verified row.

Query params: none.

Sample 200 response (click to expand)
{
  "categories": ["truck"]
}

curl

curl -H "Authorization: Bearer $TOK" \
  http://<host>:3000/api/v1/categories

Caching: Cache-Control: public, max-age=300, stale-while-revalidate=60

GET/api/v1/manufacturers

List manufacturers with verified vehicles, optionally constrained to a category.

Query params

nametyperequireddefaultnotes
categoryenumforklift | passenger_car | truck | bus
Sample 200 response (click to expand)
{
  "manufacturers": [
    { "id": 5, "name": "Dongfeng Liuzhou Motor", "country_code": "CN",
      "website": null, "vehicle_count": 1 }
  ],
  "category": "truck"
}

curl

curl -H "Authorization: Bearer $TOK" \
  "http://<host>:3000/api/v1/manufacturers?category=truck"

Caching: Cache-Control: public, max-age=300, stale-while-revalidate=60

GET/api/v1/vehicles/{slug}

Full detail for a single verified vehicle, including bilingual description, all typed specs, category-specific specs_jsonb, supplier extras, and signed-URL document download links (1-hour TTL).

Path params

nametyperequireddefaultnotes
slugstringyesVehicle slug, e.g. dongfeng-chenglong-m3-6x4-2024

Query params

nametyperequireddefaultnotes
languageenumautoen | th
Sample 200 response (click to expand)
{
  "vehicle": {
    "id": 9,
    "slug": "dongfeng-chenglong-m3-6x4-2024",
    "category": "truck",
    "manufacturer": { "id": 5, "name": "Dongfeng Liuzhou Motor" },
    "model_name": "Chenglong M3",
    "description_full": "Heavy-duty electric truck chassis with 6x4 axle...",
    "features": null,
    "available_b2b_thailand": true,
    "available_b2c_thailand": false,
    "country_of_origin": "CN",
    "specs": { /* category-specific */ },
    "supplier_extras": { /* free-form */ },
    "documents": [
      { "id": 3, "doc_type": "spec_sheet", "file_name": "chenglong-m3.pdf",
        "download_path": "/api/files/...", "expires_at": "2026-06-17T16:30:00.000Z" }
    ]
    /* ...all typed columns (battery, motor, charging, dimensions, performance) */
  },
  "language": "en"
}

curl

curl -H "Authorization: Bearer $TOK" \
  "http://<host>:3000/api/v1/vehicles/dongfeng-chenglong-m3-6x4-2024?language=th"

Returns 404 if the slug is unknown OR refers to a draft / rejected row.

POST/api/v1/recommend

Natural-language query → ranked vehicle list with reasoning. Pre-filters candidates by category guess, then calls the LLM to rank. The LLM is locked to candidate slugs — it will return an empty list with an explanation if nothing in the verified catalog fits.

EN, TH, and mixed-language queries supported. Language auto-detected from the body unless explicit.

Body (JSON)

fieldtyperequirednotes
querystringyesNatural-language use case
maxinteger (1–20)Default 3
languageenumen | th — overrides auto-detect
Sample 200 response (click to expand)
{
  "query": "I need a heavy electric truck for industrial hauling in Thailand",
  "language": "en",
  "constraints": { "category": "truck" },
  "candidates_considered": 1,
  "vehicles": [
    {
      "vehicle": { /* VehicleListItem */ },
      "score": 95,
      "pros": ["6x4 axle matches heavy-haul need", "25-ton GVWR", "Available B2B in Thailand"],
      "cons": ["Limited public real-world range data"]
    }
  ],
  "reasoning": "The Chenglong M3 is the only verified heavy electric truck...",
  "empty": false,
  "llm": { "endpoint_label": "openrouter", "model_id": "anthropic/claude-sonnet-4",
           "prompt_key": "recommend.use_case", "prompt_version": 1 }
}

curl

curl -X POST -H "Authorization: Bearer $TOK" -H "content-type: application/json" \
  -d '{"query":"heavy electric truck for hauling","max":3}' \
  http://<host>:3000/api/v1/recommend

GET/api/v1/compare

Side-by-side comparison for up to 10 slugs. Returns shared-field rows + category-specific rows when all slugs share a category. Drafts and unknown slugs are reported in not_found, never silently dropped.

Query params

nametyperequireddefaultnotes
slugsstring (comma-sep)yesUp to 10
languageenumautoen | th
Sample 200 response (click to expand)
{
  "vehicles": [ /* matched verified vehicles */ ],
  "shared_rows": [ { "field": "battery_kwh", "label": "Battery (kWh)", "values": [62.5] } ],
  "category_rows": [ /* specs_jsonb if all same category */ ],
  "not_found": ["tesla-model-y-rwd-2025", "no-such-slug"],
  "language": "en"
}

curl

curl -H "Authorization: Bearer $TOK" \
  "http://<host>:3000/api/v1/compare?slugs=dongfeng-chenglong-m3-6x4-2024,no-such-slug"

GET/api/v1/similar/{slug}

Vehicles similar to the target by cosine similarity over per-category numeric spec vector. Same-category, verified-only, deterministic. No LLM, no hallucination risk.

Path params

nametyperequireddefaultnotes
slugstringyes

Query params

nametyperequireddefaultnotes
countinteger (1–50)5
languageenumautoen | th
Sample 200 response (click to expand)
{
  "target_slug": "dongfeng-chenglong-m3-6x4-2024",
  "count": 0,
  "results": [],
  "language": "en"
}

GET/api/v1/thailand-availability/{slug}

Thailand availability split B2B / B2C, plus availability enum, country of origin, manufacturer, and notes.

Sample 200 response (click to expand)
{
  "slug": "dongfeng-chenglong-m3-6x4-2024",
  "model_name": "Chenglong M3",
  "manufacturer": "Dongfeng Liuzhou Motor",
  "available_b2b_thailand": true,
  "available_b2c_thailand": false,
  "availability": "available",
  "country_of_origin": "CN",
  "notes": "..."
}

/api/tokens (admin)

Admin-only endpoints for managing Bearer tokens. SSO-gated, not Bearer-auth — these are for the settings UI, not for consumer apps.

  • GET /api/tokens — list all tokens (including revoked).
  • POST /api/tokens — body { "label": "..." }201 with full token row including plaintext.
  • DELETE /api/tokens/{id} — soft revoke (sets revoked_at).

MCP server

Model Context Protocol server for LLM-native consumers (OpenWebUI Pipelines, custom apps with MCP-aware LLM clients). Hosted as a separate service in the compose stack, on port 4002.

Connection

Endpoints

nametyperequireddefaultnotes
GET /healthno authHealthcheck. Returns { ok: true, service, version }.
POST /mcpyesBearerStreamable HTTP transport — JSON-RPC over HTTP+SSE.
GET /mcpyesBearerServer-initiated SSE stream (rarely needed in stateless mode).
DELETE /mcpyesBearerSession shutdown — no-op in stateless mode.

Sample client (TypeScript / Node)

tsimport { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

const TOKEN = process.env.MCP_TOKEN;  // "evs_…" issued at /settings/tokens
const transport = new StreamableHTTPClientTransport(
  new URL("http://<host>:4002/mcp"),
  { requestInit: { headers: { Authorization: `Bearer ${TOKEN}` } } },
);
const client = new Client(
  { name: "my-app", version: "0.1.0" },
  { capabilities: {} },
);
await client.connect(transport);

const { tools } = await client.listTools();
console.log(tools.map((t) => t.name));

const result = await client.callTool({
  name: "recommend_for_use_case",
  arguments: { query: "I need a heavy electric truck for hauling", max: 3 },
});
const payload = JSON.parse(result.content[0].text);
console.log(payload.vehicles);
await client.close();

OpenWebUI Pipelines

In OpenWebUI's MCP Pipelines config, point at http://<host>:4002/mcp with the Bearer header set to a token from /settings/tokens. All 8 tools become available to the model as functions.

Tools (v1)

Each tool's input schema and detailed description ship in the MCP protocol itself — call client.listTools() for the live, authoritative copy. Summaries below are for orientation.

search_vehicles

Filter the verified EV catalog by typed criteria.

get_vehicle

Full detail for one verified vehicle by slug.

recommend_for_use_case

NL query → ranked list with reasoning (never fabricates).

compare_vehicles

Side-by-side comparison for up to 10 slugs.

find_similar

Cosine similarity over per-category spec vector.

check_thailand_availability

B2B / B2C availability + country of origin + notes.

list_categories

Categories with at least one verified vehicle.

list_manufacturers

Manufacturers with vehicle counts, optionally per category.

Tool result shape

Every tool returns a CallToolResult:

ts{
  content: [{ type: "text", text: "<JSON-stringified result>" }],
  structuredContent: { /* same payload as a typed object — convenience for LLM clients */ }
}

Error shape & status codes

REST errors:

json{ "error": { "code": "unauthorized", "message": "Missing or invalid Bearer token" } }

Codes

nametyperequireddefaultnotes
401 unauthorizedMissing / invalid / revoked Bearer.
404 not_foundUnknown slug, or slug refers to a draft / rejected row.
400 bad_requestMissing required field, malformed body, slug count > 10 on compare, etc.
500 internalUnhandled server error. Check /settings/activity for the error row.

MCP errors follow the JSON-RPC 2.0 spec (error: { code, message }). Auth errors at the connection layer come back as HTTP 401 before the JSON-RPC layer engages.

Notes

Bilingual handling

Detection order: explicit language param (REST query / POST body or MCP arg) → Thai script in the request body → Accept-Language header → default en. Thai-script detection is strict: any code point in U+0E00–U+0E7F in the request body flips the response to th. Mixed-script queries (e.g. "รถบรรทุก EV brand BYD") return Thai responses; brand names stay in their original form.

Cache headers

Stable-list endpoints (/categories, /manufacturers, /vehicles search) return Cache-Control: public, max-age=300, stale-while-revalidate=60. Per-row and LLM-backed endpoints return no-store.

Rate limits

None in v1. Add one if a misbehaving client starts hammering /api/v1/recommend (LLM-cost endpoint). v1.1 candidate.

Verified-only enforcement

Every read path hardcodes verification_status = 'verified'. Drafts and rejected rows are never returned via REST or MCP. To see drafts, use the ingest review UI.

Observability

Every API + MCP call writes a row to activity_log. View at /settings/activity — filter by kind=api_call or kind=mcp_call. LLM calls (kind=recommend etc.) include prompt/completion/total token counts for cost tracking.

Last updated: 2026-06-17. If you spot a discrepancy between this page and the live API, the live code wins — open an issue and fix the docs in the same PR.