@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.
- package/README.md +137 -0
- package/dist/api-client.d.ts +255 -0
- package/dist/api-client.js +260 -0
- package/dist/api-client.js.map +1 -0
- package/dist/methodology-data.d.ts +40 -0
- package/dist/methodology-data.js +179 -0
- package/dist/methodology-data.js.map +1 -0
- package/dist/server.d.ts +28 -0
- package/dist/server.js +173 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/area-brief-audiences.d.ts +40 -0
- package/dist/tools/area-brief-audiences.js +131 -0
- package/dist/tools/area-brief-audiences.js.map +1 -0
- package/dist/tools/area-brief-format.d.ts +15 -0
- package/dist/tools/area-brief-format.js +142 -0
- package/dist/tools/area-brief-format.js.map +1 -0
- package/dist/tools/area-brief.d.ts +48 -0
- package/dist/tools/area-brief.js +82 -0
- package/dist/tools/area-brief.js.map +1 -0
- package/dist/tools/compare-postcodes.d.ts +57 -0
- package/dist/tools/compare-postcodes.js +148 -0
- package/dist/tools/compare-postcodes.js.map +1 -0
- package/dist/tools/engine-version.d.ts +23 -0
- package/dist/tools/engine-version.js +35 -0
- package/dist/tools/engine-version.js.map +1 -0
- package/dist/tools/find-areas.d.ts +38 -0
- package/dist/tools/find-areas.js +58 -0
- package/dist/tools/find-areas.js.map +1 -0
- package/dist/tools/find-peers.d.ts +44 -0
- package/dist/tools/find-peers.js +77 -0
- package/dist/tools/find-peers.js.map +1 -0
- package/dist/tools/get-area-signals.d.ts +39 -0
- package/dist/tools/get-area-signals.js +60 -0
- package/dist/tools/get-area-signals.js.map +1 -0
- package/dist/tools/get-portfolio-changes.d.ts +58 -0
- package/dist/tools/get-portfolio-changes.js +121 -0
- package/dist/tools/get-portfolio-changes.js.map +1 -0
- package/dist/tools/get-signals-by-category.d.ts +44 -0
- package/dist/tools/get-signals-by-category.js +67 -0
- package/dist/tools/get-signals-by-category.js.map +1 -0
- package/dist/tools/intelligence-format.d.ts +28 -0
- package/dist/tools/intelligence-format.js +197 -0
- package/dist/tools/intelligence-format.js.map +1 -0
- package/dist/tools/methodology-for.d.ts +34 -0
- package/dist/tools/methodology-for.js +71 -0
- package/dist/tools/methodology-for.js.map +1 -0
- package/dist/tools/score-postcode.d.ts +48 -0
- package/dist/tools/score-postcode.js +108 -0
- package/dist/tools/score-postcode.js.map +1 -0
- package/dist/tools/signals-format.d.ts +11 -0
- package/dist/tools/signals-format.js +116 -0
- package/dist/tools/signals-format.js.map +1 -0
- package/dist/tools/watch-portfolio.d.ts +50 -0
- package/dist/tools/watch-portfolio.js +126 -0
- package/dist/tools/watch-portfolio.js.map +1 -0
- 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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"}
|