@oga-mcp/server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +137 -0
  2. package/dist/api-client.d.ts +255 -0
  3. package/dist/api-client.js +260 -0
  4. package/dist/api-client.js.map +1 -0
  5. package/dist/methodology-data.d.ts +40 -0
  6. package/dist/methodology-data.js +179 -0
  7. package/dist/methodology-data.js.map +1 -0
  8. package/dist/server.d.ts +28 -0
  9. package/dist/server.js +173 -0
  10. package/dist/server.js.map +1 -0
  11. package/dist/tools/area-brief-audiences.d.ts +40 -0
  12. package/dist/tools/area-brief-audiences.js +131 -0
  13. package/dist/tools/area-brief-audiences.js.map +1 -0
  14. package/dist/tools/area-brief-format.d.ts +15 -0
  15. package/dist/tools/area-brief-format.js +142 -0
  16. package/dist/tools/area-brief-format.js.map +1 -0
  17. package/dist/tools/area-brief.d.ts +48 -0
  18. package/dist/tools/area-brief.js +82 -0
  19. package/dist/tools/area-brief.js.map +1 -0
  20. package/dist/tools/compare-postcodes.d.ts +57 -0
  21. package/dist/tools/compare-postcodes.js +148 -0
  22. package/dist/tools/compare-postcodes.js.map +1 -0
  23. package/dist/tools/engine-version.d.ts +23 -0
  24. package/dist/tools/engine-version.js +35 -0
  25. package/dist/tools/engine-version.js.map +1 -0
  26. package/dist/tools/find-areas.d.ts +38 -0
  27. package/dist/tools/find-areas.js +58 -0
  28. package/dist/tools/find-areas.js.map +1 -0
  29. package/dist/tools/find-peers.d.ts +44 -0
  30. package/dist/tools/find-peers.js +77 -0
  31. package/dist/tools/find-peers.js.map +1 -0
  32. package/dist/tools/get-area-signals.d.ts +39 -0
  33. package/dist/tools/get-area-signals.js +60 -0
  34. package/dist/tools/get-area-signals.js.map +1 -0
  35. package/dist/tools/get-portfolio-changes.d.ts +58 -0
  36. package/dist/tools/get-portfolio-changes.js +121 -0
  37. package/dist/tools/get-portfolio-changes.js.map +1 -0
  38. package/dist/tools/get-signals-by-category.d.ts +44 -0
  39. package/dist/tools/get-signals-by-category.js +67 -0
  40. package/dist/tools/get-signals-by-category.js.map +1 -0
  41. package/dist/tools/intelligence-format.d.ts +28 -0
  42. package/dist/tools/intelligence-format.js +197 -0
  43. package/dist/tools/intelligence-format.js.map +1 -0
  44. package/dist/tools/methodology-for.d.ts +34 -0
  45. package/dist/tools/methodology-for.js +71 -0
  46. package/dist/tools/methodology-for.js.map +1 -0
  47. package/dist/tools/score-postcode.d.ts +48 -0
  48. package/dist/tools/score-postcode.js +108 -0
  49. package/dist/tools/score-postcode.js.map +1 -0
  50. package/dist/tools/signals-format.d.ts +11 -0
  51. package/dist/tools/signals-format.js +116 -0
  52. package/dist/tools/signals-format.js.map +1 -0
  53. package/dist/tools/watch-portfolio.d.ts +50 -0
  54. package/dist/tools/watch-portfolio.js +126 -0
  55. package/dist/tools/watch-portfolio.js.map +1 -0
  56. package/package.json +48 -0
package/README.md ADDED
@@ -0,0 +1,137 @@
1
+ # `@oga-mcp/server`
2
+
3
+ OneGoodArea MCP server — UK area intelligence inside Claude Desktop, Cursor, Windsurf, or any MCP-compatible client.
4
+
5
+ Score any UK postcode (or place name) for residential mortgage origination, retail site selection, property investment, or as a neutral reference baseline. Driven by the same engine that powers https://www.onegoodarea.com — five weighted dimensions per preset, confidence per dimension with engine-grounded reasoning, source attribution, public methodology.
6
+
7
+ ---
8
+
9
+ ## Install
10
+
11
+ You don't install it directly. Configure your MCP client to spawn it via `npx`.
12
+
13
+ ### Claude Desktop
14
+
15
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
16
+
17
+ ```json
18
+ {
19
+ "mcpServers": {
20
+ "onegoodarea": {
21
+ "command": "npx",
22
+ "args": ["-y", "@oga-mcp/server"],
23
+ "env": {
24
+ "OOGA_API_KEY": "oga_xxx"
25
+ }
26
+ }
27
+ }
28
+ }
29
+ ```
30
+
31
+ Restart Claude Desktop. The OneGoodArea tools appear when you start a conversation about a UK location.
32
+
33
+ ### Cursor
34
+
35
+ Add to `.cursor/mcp.json` in your project (or global config):
36
+
37
+ ```json
38
+ {
39
+ "mcpServers": {
40
+ "onegoodarea": {
41
+ "command": "npx",
42
+ "args": ["-y", "@oga-mcp/server"],
43
+ "env": { "OOGA_API_KEY": "oga_xxx" }
44
+ }
45
+ }
46
+ }
47
+ ```
48
+
49
+ ---
50
+
51
+ ## Get an API key
52
+
53
+ 1. Sign up at https://www.onegoodarea.com/sign-up (Sandbox tier is free, 35 calls/month for evaluation).
54
+ 2. Go to https://www.onegoodarea.com/dashboard.
55
+ 3. Create an API key — it starts with `oga_`.
56
+ 4. Paste into the `OOGA_API_KEY` env var above.
57
+
58
+ For higher volume, upgrade at https://www.onegoodarea.com/pricing.
59
+
60
+ ---
61
+
62
+ ## Tools
63
+
64
+ Every tool's response is composed server-side from real engine state. No client-side text synthesis: the prose you see comes from the deterministic engine that produced the score.
65
+
66
+ ### `score_postcode(area, preset)`
67
+
68
+ Scores a UK postcode (or place name) for one of four decision presets.
69
+
70
+ **Arguments:**
71
+
72
+ - `area` (string, required): UK postcode like `"SW1A 1AA"` or place name like `"Manchester city centre"`. Max 100 characters.
73
+ - `preset` (string, required): One of:
74
+ - `moving` — origination scoring (residential mortgage suitability, demand-side risk)
75
+ - `business` — site selection (footfall, competition, commercial viability)
76
+ - `investing` — investment scoring (yield, growth, regeneration)
77
+ - `research` — reference scoring (neutral baseline, equal weights)
78
+
79
+ **Returns:** Markdown with the overall 0-100 score, five weighted dimensions with the engine's per-dimension reasoning and confidence reason, a server-composed one-paragraph summary, actionable recommendations from low-scoring or low-confidence dimensions, and the list of public datasets that contributed.
80
+
81
+ ### `compare_postcodes(areas, preset)`
82
+
83
+ Scores 2-8 UK areas side-by-side for the same preset.
84
+
85
+ **Arguments:**
86
+
87
+ - `areas` (string[], required): 2-8 UK postcodes or place names.
88
+ - `preset` (string, required): Same preset values as `score_postcode`.
89
+
90
+ **Returns:** A sorted comparison table (rank, area, score, area type, top dimension) plus per-area summaries from the engine. Partial failures are surfaced inline rather than failing the whole call.
91
+
92
+ ### `methodology_for(dimension)`
93
+
94
+ Explains how a specific scoring dimension is computed. Static lookup — no network, no quota cost.
95
+
96
+ ### `engine_version()`
97
+
98
+ Returns the current engine version + changelog. Static lookup. The live engine version is also echoed on every `score_postcode` response.
99
+
100
+ ---
101
+
102
+ ## Pricing
103
+
104
+ The MCP server itself is free. API calls go through your OneGoodArea plan:
105
+
106
+ - Sandbox £0/mo · 35 API calls — evaluation only
107
+ - Starter £49/mo · 1,500 calls
108
+ - Build £149/mo · 6,000 calls
109
+ - Scale £499/mo · 25,000 calls
110
+ - Growth £1,499/mo · 100,000 calls — includes MCP server access at no extra cost
111
+ - Enterprise from £4,999/mo · 250,000+ calls — includes MCP server access
112
+
113
+ For Sandbox / Starter / Build / Scale, MCP access is a £29/mo add-on.
114
+
115
+ ---
116
+
117
+ ## Development
118
+
119
+ ```sh
120
+ cd mcp
121
+ npm install
122
+ npm run dev # run via tsx, reads OOGA_API_KEY from env
123
+ npm test # vitest
124
+ npm run build # tsc to dist/
125
+ ```
126
+
127
+ Override the API base for local dev against the standalone API server:
128
+
129
+ ```sh
130
+ OOGA_API_BASE=http://localhost:4000 OOGA_API_KEY=oga_dev npm run dev
131
+ ```
132
+
133
+ ---
134
+
135
+ ## License
136
+
137
+ MIT. © 2026 OneGoodArea.
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Thin HTTP client for the OneGoodArea REST API.
3
+ * Used by every MCP tool — keeps auth + base URL handling in one place.
4
+ *
5
+ * AR-364: rewritten to target the live apps/api at /v1/* directly (not
6
+ * the apps/web /api/v1/* proxy, which is silently broken since AR-324).
7
+ * Uses /v1/score?explain=true so brief-shape narrative is composed
8
+ * server-side from real engine state — no client-side text synthesis.
9
+ */
10
+ export type Preset = "moving" | "business" | "investing" | "research";
11
+ /** AR-366: the seven signal categories exposed by /v1/signals/:category.
12
+ Mirrors @onegoodarea/contracts SIGNAL_CATEGORIES. */
13
+ export declare const SIGNAL_CATEGORIES: readonly ["crime", "deprivation", "property", "schools", "amenities", "transport", "environment"];
14
+ export type SignalCategory = typeof SIGNAL_CATEGORIES[number];
15
+ /** AR-366: one addressable signal as returned by /v1/area + /v1/signals/:category.
16
+ Matches @onegoodarea/contracts Signal. */
17
+ export interface OogaSignal {
18
+ key: string;
19
+ category: SignalCategory;
20
+ label: string;
21
+ value: number | string | null;
22
+ unit: string | null;
23
+ normalized_value?: number | null;
24
+ percentile?: number | null;
25
+ direction: "higher_is_better" | "lower_is_better" | "neutral";
26
+ confidence: number;
27
+ confidence_reason: string;
28
+ source: string;
29
+ observed_period: string;
30
+ }
31
+ /** AR-367: the seven Intelligence plan ops. Mirrors @onegoodarea/contracts
32
+ QueryPlan.op discriminator. */
33
+ export type OogaPlanOp = "rank_areas" | "get_area" | "score_area" | "compare_areas" | "find_peers" | "find_insights" | "find_forecast";
34
+ /** AR-367: response from POST /v1/query — discriminated by plan.op. We
35
+ don't strict-decode the per-op results shapes here; the formatter
36
+ walks them defensively. Matches @onegoodarea/contracts QueryResponse. */
37
+ export interface OogaQueryResponse {
38
+ plan: {
39
+ op: OogaPlanOp;
40
+ params: unknown;
41
+ };
42
+ plan_source: "client" | "nl";
43
+ results: unknown;
44
+ meta: {
45
+ generated_at: string;
46
+ };
47
+ }
48
+ /** AR-368: Monitor types — matches @onegoodarea/contracts Portfolio + PortfolioArea. */
49
+ export interface OogaPortfolio {
50
+ id: string;
51
+ name: string;
52
+ area_count?: number;
53
+ created_at?: string;
54
+ }
55
+ export interface OogaPortfolioArea {
56
+ id: string;
57
+ area: string;
58
+ label: string | null;
59
+ created_at?: string;
60
+ }
61
+ /** AR-368: response from POST /v1/portfolios/:id/areas (the route returns
62
+ the updated portfolio detail with all areas). */
63
+ export interface OogaPortfolioDetail extends OogaPortfolio {
64
+ areas: OogaPortfolioArea[];
65
+ }
66
+ /** AR-368: a material signal change for one tracked area between two
67
+ time-series periods. Mirrors @onegoodarea/contracts SignalChange. */
68
+ export interface OogaSignalChange {
69
+ signal_key: string;
70
+ label: string | null;
71
+ area: string;
72
+ geo_code: string;
73
+ period_from: string;
74
+ period_to: string;
75
+ value_from: number | null;
76
+ value_to: number | null;
77
+ delta: number | null;
78
+ pct_change: number | null;
79
+ direction: "up" | "down" | "flat";
80
+ material: boolean;
81
+ }
82
+ /** AR-368: the response from POST /v1/portfolios/:id/changes — a change
83
+ detection report for one portfolio between two periods. */
84
+ export interface OogaChangeReport {
85
+ portfolio_id: string;
86
+ baseline: "previous" | "first";
87
+ threshold_pct: number;
88
+ min_transactions: number;
89
+ areas_checked: number;
90
+ material_count: number;
91
+ changes: OogaSignalChange[];
92
+ generated_at: string;
93
+ }
94
+ /** AR-367: typed peers response from POST /v1/peers. */
95
+ export interface OogaPeersResponse {
96
+ target: {
97
+ geo_code: string;
98
+ signals_used: string[];
99
+ };
100
+ peers: Array<{
101
+ geo_code: string;
102
+ distance: number;
103
+ n_dims_used: number;
104
+ }>;
105
+ meta: {
106
+ generated_at: string;
107
+ scope: string;
108
+ };
109
+ }
110
+ /** AR-366: the AreaProfile response from /v1/area and /v1/signals/:category. */
111
+ export interface OogaAreaProfile {
112
+ geo: {
113
+ query: string;
114
+ postcode: string | null;
115
+ latitude: number;
116
+ longitude: number;
117
+ lsoa: string | null;
118
+ msoa: string | null;
119
+ admin_district: string | null;
120
+ region: string | null;
121
+ country: string;
122
+ area_type: "urban" | "suburban" | "rural";
123
+ };
124
+ signals: OogaSignal[];
125
+ meta: {
126
+ engine_version: string;
127
+ generated_at: string;
128
+ sources: string[];
129
+ fetch_mode: "live" | "store" | "hybrid";
130
+ };
131
+ }
132
+ /** One weighted component of a composite score. Matches the
133
+ @onegoodarea/contracts `ScoreDimension` shape; we redeclare it here
134
+ so the MCP package can be published independently of the monorepo
135
+ contracts (the npm release ships only mcp/dist). */
136
+ export interface OogaScoreDimension {
137
+ key: string;
138
+ label: string;
139
+ score: number;
140
+ weight: number;
141
+ confidence: number;
142
+ reasoning: string;
143
+ confidence_reason: string;
144
+ }
145
+ /** The response of POST /v1/score?explain=true. The `summary`,
146
+ `recommendations`, and `data_sources` fields are server-side
147
+ composed when explain mode is on (AR-363). */
148
+ export interface OogaScoreResponse {
149
+ area: string;
150
+ preset: Preset;
151
+ score: number;
152
+ area_type: "urban" | "suburban" | "rural";
153
+ dimensions: OogaScoreDimension[];
154
+ confidence: number;
155
+ weights_source: "preset" | "custom";
156
+ engine_version: string;
157
+ /** Brief-shape fields — present when ?explain=true. */
158
+ summary?: string;
159
+ recommendations?: string[];
160
+ data_sources?: string[];
161
+ }
162
+ export interface OogaApiClientOptions {
163
+ apiKey: string;
164
+ baseUrl?: string;
165
+ /** Defaults to 60 seconds — the engine takes 15-45s on cache miss. */
166
+ timeoutMs?: number;
167
+ }
168
+ /** /v1/me response shape (the bits MCP relies on). The API returns more
169
+ fields (org, key allowlist, addons, etc.) — we only declare what we
170
+ use, since extra fields are fine at runtime. */
171
+ export interface OogaMeResponse {
172
+ plan: string;
173
+ plan_name: string;
174
+ api_access: boolean;
175
+ mcp_access: boolean;
176
+ api_calls_per_month: number;
177
+ used_this_month: number;
178
+ limit_this_month: number | null;
179
+ engine_version: string;
180
+ }
181
+ export declare class OogaApiError extends Error {
182
+ readonly status?: number | undefined;
183
+ readonly responseBody?: unknown | undefined;
184
+ constructor(message: string, status?: number | undefined, responseBody?: unknown | undefined);
185
+ }
186
+ export declare class OogaApiClient {
187
+ private readonly apiKey;
188
+ private readonly baseUrl;
189
+ private readonly timeoutMs;
190
+ constructor(opts: OogaApiClientOptions);
191
+ /**
192
+ * GET /v1/me — returns the authenticated user's plan + entitlements.
193
+ * Called by the MCP server at startup to check `mcp_access`.
194
+ */
195
+ me(): Promise<OogaMeResponse>;
196
+ /**
197
+ * POST /v1/score?explain=true — score a UK area for the given preset.
198
+ * `explain=true` triggers server-side composition of the brief-shape
199
+ * fields (summary, recommendations, data_sources) from real engine
200
+ * state. Per the brief-shape policy, the MCP never synthesises text.
201
+ */
202
+ scoreArea(area: string, preset: Preset): Promise<OogaScoreResponse>;
203
+ /**
204
+ * GET /v1/area?area=<area> — full signal catalog for an area.
205
+ * Returns every signal with its raw value, normalized value + percentile
206
+ * (when store-backed), per-signal confidence + reason, source attribution,
207
+ * and observation period. The full Signals primitive (AR-366).
208
+ */
209
+ getAreaSignals(area: string): Promise<OogaAreaProfile>;
210
+ /**
211
+ * GET /v1/signals/:category?area=<area> — signals filtered to one category
212
+ * (crime / deprivation / property / schools / amenities / transport /
213
+ * environment). Same Signal shape as /v1/area; just a narrower payload
214
+ * for when the LLM only needs one slice.
215
+ */
216
+ getSignalsByCategory(area: string, category: SignalCategory): Promise<OogaAreaProfile>;
217
+ /**
218
+ * POST /v1/query with `{question}` — natural-language interface to the
219
+ * Intelligence query plane. The planner emits a typed plan; the DB
220
+ * executes it; the response carries plan + plan_source + results so
221
+ * every answer is reproducible (AR-367).
222
+ */
223
+ findAreas(question: string): Promise<OogaQueryResponse>;
224
+ /**
225
+ * POST /v1/peers with `{target: {area}, k?}` — k-NN over normalized
226
+ * signal values. Returns the target geo_code + signals_used + a
227
+ * ranked peers[] list (AR-367).
228
+ */
229
+ findPeers(area: string, k?: number): Promise<OogaPeersResponse>;
230
+ /**
231
+ * POST /v1/portfolios — create a new Monitor portfolio. AR-368.
232
+ */
233
+ createPortfolio(name: string): Promise<OogaPortfolio>;
234
+ /**
235
+ * POST /v1/portfolios/:id/areas — add tracked areas to a portfolio. Returns
236
+ * the updated portfolio detail with all areas. AR-368.
237
+ */
238
+ addPortfolioAreas(portfolioId: string, areas: Array<{
239
+ area: string;
240
+ label?: string | null;
241
+ }>): Promise<OogaPortfolioDetail>;
242
+ /**
243
+ * POST /v1/portfolios/:id/changes — detect material signal changes between
244
+ * two time-series periods. AR-368.
245
+ */
246
+ getPortfolioChanges(portfolioId: string, opts?: {
247
+ baseline?: "previous" | "first";
248
+ threshold_pct?: number;
249
+ min_transactions?: number;
250
+ }): Promise<OogaChangeReport>;
251
+ /** Shared POST handler for the Intelligence endpoints. */
252
+ private postIntelligence;
253
+ /** Shared GET handler for the two area-profile endpoints. */
254
+ private getAreaProfile;
255
+ }
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Thin HTTP client for the OneGoodArea REST API.
3
+ * Used by every MCP tool — keeps auth + base URL handling in one place.
4
+ *
5
+ * AR-364: rewritten to target the live apps/api at /v1/* directly (not
6
+ * the apps/web /api/v1/* proxy, which is silently broken since AR-324).
7
+ * Uses /v1/score?explain=true so brief-shape narrative is composed
8
+ * server-side from real engine state — no client-side text synthesis.
9
+ */
10
+ const DEFAULT_BASE = "https://onegoodarea.onrender.com";
11
+ const USER_AGENT = "onegoodarea-mcp-server/1.0.0";
12
+ /** AR-366: the seven signal categories exposed by /v1/signals/:category.
13
+ Mirrors @onegoodarea/contracts SIGNAL_CATEGORIES. */
14
+ export const SIGNAL_CATEGORIES = [
15
+ "crime",
16
+ "deprivation",
17
+ "property",
18
+ "schools",
19
+ "amenities",
20
+ "transport",
21
+ "environment",
22
+ ];
23
+ export class OogaApiError extends Error {
24
+ status;
25
+ responseBody;
26
+ constructor(message, status, responseBody) {
27
+ super(message);
28
+ this.status = status;
29
+ this.responseBody = responseBody;
30
+ this.name = "OogaApiError";
31
+ }
32
+ }
33
+ export class OogaApiClient {
34
+ apiKey;
35
+ baseUrl;
36
+ timeoutMs;
37
+ constructor(opts) {
38
+ if (!opts.apiKey)
39
+ throw new Error("OogaApiClient requires apiKey");
40
+ this.apiKey = opts.apiKey;
41
+ this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE).replace(/\/$/, "");
42
+ this.timeoutMs = opts.timeoutMs ?? 60_000;
43
+ }
44
+ /**
45
+ * GET /v1/me — returns the authenticated user's plan + entitlements.
46
+ * Called by the MCP server at startup to check `mcp_access`.
47
+ */
48
+ async me() {
49
+ const url = `${this.baseUrl}/v1/me`;
50
+ const controller = new AbortController();
51
+ const timeout = setTimeout(() => controller.abort(), 10_000);
52
+ try {
53
+ const res = await fetch(url, {
54
+ method: "GET",
55
+ headers: {
56
+ "Authorization": `Bearer ${this.apiKey}`,
57
+ "User-Agent": USER_AGENT,
58
+ },
59
+ signal: controller.signal,
60
+ });
61
+ const text = await res.text();
62
+ let body;
63
+ try {
64
+ body = JSON.parse(text);
65
+ }
66
+ catch {
67
+ body = text;
68
+ }
69
+ if (!res.ok) {
70
+ const errMsg = typeof body === "object" && body !== null && "error" in body
71
+ ? String(body.error)
72
+ : `HTTP ${res.status}`;
73
+ throw new OogaApiError(errMsg, res.status, body);
74
+ }
75
+ return body;
76
+ }
77
+ finally {
78
+ clearTimeout(timeout);
79
+ }
80
+ }
81
+ /**
82
+ * POST /v1/score?explain=true — score a UK area for the given preset.
83
+ * `explain=true` triggers server-side composition of the brief-shape
84
+ * fields (summary, recommendations, data_sources) from real engine
85
+ * state. Per the brief-shape policy, the MCP never synthesises text.
86
+ */
87
+ async scoreArea(area, preset) {
88
+ const url = `${this.baseUrl}/v1/score?explain=true`;
89
+ const controller = new AbortController();
90
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
91
+ try {
92
+ const res = await fetch(url, {
93
+ method: "POST",
94
+ headers: {
95
+ "Authorization": `Bearer ${this.apiKey}`,
96
+ "Content-Type": "application/json",
97
+ "User-Agent": USER_AGENT,
98
+ },
99
+ body: JSON.stringify({ area, preset }),
100
+ signal: controller.signal,
101
+ });
102
+ const text = await res.text();
103
+ let body;
104
+ try {
105
+ body = JSON.parse(text);
106
+ }
107
+ catch {
108
+ body = text;
109
+ }
110
+ if (!res.ok) {
111
+ const errMsg = typeof body === "object" && body !== null && "error" in body
112
+ ? String(body.error)
113
+ : `HTTP ${res.status}`;
114
+ throw new OogaApiError(errMsg, res.status, body);
115
+ }
116
+ return body;
117
+ }
118
+ finally {
119
+ clearTimeout(timeout);
120
+ }
121
+ }
122
+ /**
123
+ * GET /v1/area?area=<area> — full signal catalog for an area.
124
+ * Returns every signal with its raw value, normalized value + percentile
125
+ * (when store-backed), per-signal confidence + reason, source attribution,
126
+ * and observation period. The full Signals primitive (AR-366).
127
+ */
128
+ async getAreaSignals(area) {
129
+ return this.getAreaProfile(`/v1/area?area=${encodeURIComponent(area)}`);
130
+ }
131
+ /**
132
+ * GET /v1/signals/:category?area=<area> — signals filtered to one category
133
+ * (crime / deprivation / property / schools / amenities / transport /
134
+ * environment). Same Signal shape as /v1/area; just a narrower payload
135
+ * for when the LLM only needs one slice.
136
+ */
137
+ async getSignalsByCategory(area, category) {
138
+ return this.getAreaProfile(`/v1/signals/${category}?area=${encodeURIComponent(area)}`);
139
+ }
140
+ /**
141
+ * POST /v1/query with `{question}` — natural-language interface to the
142
+ * Intelligence query plane. The planner emits a typed plan; the DB
143
+ * executes it; the response carries plan + plan_source + results so
144
+ * every answer is reproducible (AR-367).
145
+ */
146
+ async findAreas(question) {
147
+ return this.postIntelligence("/v1/query", { question });
148
+ }
149
+ /**
150
+ * POST /v1/peers with `{target: {area}, k?}` — k-NN over normalized
151
+ * signal values. Returns the target geo_code + signals_used + a
152
+ * ranked peers[] list (AR-367).
153
+ */
154
+ async findPeers(area, k) {
155
+ const body = { target: { area } };
156
+ if (k !== undefined)
157
+ body.k = k;
158
+ return this.postIntelligence("/v1/peers", body);
159
+ }
160
+ /**
161
+ * POST /v1/portfolios — create a new Monitor portfolio. AR-368.
162
+ */
163
+ async createPortfolio(name) {
164
+ return this.postIntelligence("/v1/portfolios", { name });
165
+ }
166
+ /**
167
+ * POST /v1/portfolios/:id/areas — add tracked areas to a portfolio. Returns
168
+ * the updated portfolio detail with all areas. AR-368.
169
+ */
170
+ async addPortfolioAreas(portfolioId, areas) {
171
+ return this.postIntelligence(`/v1/portfolios/${encodeURIComponent(portfolioId)}/areas`, { areas });
172
+ }
173
+ /**
174
+ * POST /v1/portfolios/:id/changes — detect material signal changes between
175
+ * two time-series periods. AR-368.
176
+ */
177
+ async getPortfolioChanges(portfolioId, opts = {}) {
178
+ const body = {};
179
+ if (opts.baseline !== undefined)
180
+ body.baseline = opts.baseline;
181
+ if (opts.threshold_pct !== undefined)
182
+ body.threshold_pct = opts.threshold_pct;
183
+ if (opts.min_transactions !== undefined)
184
+ body.min_transactions = opts.min_transactions;
185
+ /* Default emit:false — the MCP shouldn't fire webhooks on a probe call. */
186
+ body.emit = false;
187
+ return this.postIntelligence(`/v1/portfolios/${encodeURIComponent(portfolioId)}/changes`, body);
188
+ }
189
+ /** Shared POST handler for the Intelligence endpoints. */
190
+ async postIntelligence(path, body) {
191
+ const url = `${this.baseUrl}${path}`;
192
+ const controller = new AbortController();
193
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
194
+ try {
195
+ const res = await fetch(url, {
196
+ method: "POST",
197
+ headers: {
198
+ "Authorization": `Bearer ${this.apiKey}`,
199
+ "Content-Type": "application/json",
200
+ "User-Agent": USER_AGENT,
201
+ },
202
+ body: JSON.stringify(body),
203
+ signal: controller.signal,
204
+ });
205
+ const text = await res.text();
206
+ let parsed;
207
+ try {
208
+ parsed = JSON.parse(text);
209
+ }
210
+ catch {
211
+ parsed = text;
212
+ }
213
+ if (!res.ok) {
214
+ const errMsg = typeof parsed === "object" && parsed !== null && "error" in parsed
215
+ ? String(parsed.error)
216
+ : `HTTP ${res.status}`;
217
+ throw new OogaApiError(errMsg, res.status, parsed);
218
+ }
219
+ return parsed;
220
+ }
221
+ finally {
222
+ clearTimeout(timeout);
223
+ }
224
+ }
225
+ /** Shared GET handler for the two area-profile endpoints. */
226
+ async getAreaProfile(path) {
227
+ const url = `${this.baseUrl}${path}`;
228
+ const controller = new AbortController();
229
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
230
+ try {
231
+ const res = await fetch(url, {
232
+ method: "GET",
233
+ headers: {
234
+ "Authorization": `Bearer ${this.apiKey}`,
235
+ "User-Agent": USER_AGENT,
236
+ },
237
+ signal: controller.signal,
238
+ });
239
+ const text = await res.text();
240
+ let body;
241
+ try {
242
+ body = JSON.parse(text);
243
+ }
244
+ catch {
245
+ body = text;
246
+ }
247
+ if (!res.ok) {
248
+ const errMsg = typeof body === "object" && body !== null && "error" in body
249
+ ? String(body.error)
250
+ : `HTTP ${res.status}`;
251
+ throw new OogaApiError(errMsg, res.status, body);
252
+ }
253
+ return body;
254
+ }
255
+ finally {
256
+ clearTimeout(timeout);
257
+ }
258
+ }
259
+ }
260
+ //# sourceMappingURL=api-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-client.js","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"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"}