@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
@@ -0,0 +1,58 @@
1
+ /**
2
+ * MCP tool: find_areas (AR-367)
3
+ *
4
+ * Natural-language interface to the OneGoodArea Intelligence query plane.
5
+ * The planner translates the question into a typed plan (one of 7 ops);
6
+ * the database executes it; the response carries plan + plan_source +
7
+ * results so every answer is reproducible.
8
+ *
9
+ * Wraps POST /v1/query with {question}.
10
+ */
11
+ import { OogaApiError } from "../api-client.js";
12
+ import { formatQueryResponseAsText } from "./intelligence-format.js";
13
+ export const findAreasToolName = "find_areas";
14
+ export const findAreasToolDef = {
15
+ name: findAreasToolName,
16
+ description: "Ask a natural-language question over UK area intelligence. The planner translates it into one of seven typed plan operations — rank_areas (filter + sort LSOAs by signals), get_area, score_area, compare_areas (2-5 side by side), find_peers (k-NN over normalized signals), find_insights (peer-relative anomalies), or find_forecast (linear-regression projection). " +
17
+ "Returns the emitted plan (for transparency) plus the op-specific results. Every answer is reproducible because the plan is the contract — replay the same plan, get the same result.",
18
+ inputSchema: {
19
+ type: "object",
20
+ properties: {
21
+ question: {
22
+ type: "string",
23
+ description: "Free-form question about UK areas. Examples: 'areas under £250k median price and rising YoY in England', 'compare M1 1AE, SW4 0LG, EH1 1BB for site selection', 'LSOAs like E01034129 with similar crime + amenity profile', 'forecast median price in M1 1AE over the next 12 months'. Max 500 characters.",
24
+ },
25
+ },
26
+ required: ["question"],
27
+ additionalProperties: false,
28
+ },
29
+ };
30
+ export function parseFindAreasArgs(raw) {
31
+ if (typeof raw !== "object" || raw === null) {
32
+ throw new Error("find_areas arguments must be an object");
33
+ }
34
+ const obj = raw;
35
+ const question = obj.question;
36
+ if (typeof question !== "string" || question.trim().length === 0) {
37
+ throw new Error("question must be a non-empty string");
38
+ }
39
+ if (question.length > 500) {
40
+ throw new Error("question must be 500 characters or fewer");
41
+ }
42
+ return { question: question.trim() };
43
+ }
44
+ export async function executeFindAreas(client, args) {
45
+ try {
46
+ const result = await client.findAreas(args.question);
47
+ return { content: [{ type: "text", text: formatQueryResponseAsText(result) }] };
48
+ }
49
+ catch (err) {
50
+ if (err instanceof OogaApiError) {
51
+ const msg = `OneGoodArea API error (HTTP ${err.status ?? "?"}): ${err.message}`;
52
+ return { content: [{ type: "text", text: msg }], isError: true };
53
+ }
54
+ const msg = err instanceof Error ? err.message : String(err);
55
+ return { content: [{ type: "text", text: `Tool error: ${msg}` }], isError: true };
56
+ }
57
+ }
58
+ //# sourceMappingURL=find-areas.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"find-areas.js","sourceRoot":"","sources":["../../src/tools/find-areas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAC;AAErE,MAAM,CAAC,MAAM,iBAAiB,GAAG,YAAY,CAAC;AAE9C,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,IAAI,EAAE,iBAAiB;IACvB,WAAW,EACT,2WAA2W;QAC3W,sLAAsL;IACxL,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE;YACV,QAAQ,EAAE;gBACR,IAAI,EAAE,QAAQ;gBACd,WAAW,EACT,6SAA6S;aAChT;SACF;QACD,QAAQ,EAAE,CAAC,UAAU,CAAC;QACtB,oBAAoB,EAAE,KAAK;KAC5B;CACO,CAAC;AAMX,MAAM,UAAU,kBAAkB,CAAC,GAAY;IAC7C,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QAC5C,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;IAC5D,CAAC;IACD,MAAM,GAAG,GAAG,GAA8B,CAAC;IAC3C,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC;IAE9B,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACjE,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;IACzD,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;IAC9D,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;AACvC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,MAAqB,EACrB,IAAmB;IAEnB,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACrD,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,yBAAyB,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;IAClF,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,YAAY,EAAE,CAAC;YAChC,MAAM,GAAG,GAAG,+BAA+B,GAAG,CAAC,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,OAAO,EAAE,CAAC;YAChF,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QACnE,CAAC;QACD,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC7D,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,EAAE,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IACpF,CAAC;AACH,CAAC"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * MCP tool: find_peers (AR-367)
3
+ *
4
+ * k-nearest-neighbour peers for a UK area, by normalized signal values.
5
+ * Returns the target's geo_code + signals_used + a ranked peers[] list
6
+ * with distance and how many signal dimensions contributed.
7
+ *
8
+ * Wraps POST /v1/peers.
9
+ */
10
+ import type { OogaApiClient } from "../api-client.js";
11
+ export declare const findPeersToolName = "find_peers";
12
+ export declare const findPeersToolDef: {
13
+ readonly name: "find_peers";
14
+ readonly description: string;
15
+ readonly inputSchema: {
16
+ readonly type: "object";
17
+ readonly properties: {
18
+ readonly area: {
19
+ readonly type: "string";
20
+ readonly description: "UK postcode (e.g. 'SW1A 1AA') or place name (e.g. 'Manchester city centre'). Max 100 characters.";
21
+ };
22
+ readonly k: {
23
+ readonly type: "number";
24
+ readonly description: "Number of peers to return. Default 20, max 200.";
25
+ readonly minimum: 1;
26
+ readonly maximum: 200;
27
+ };
28
+ };
29
+ readonly required: readonly ["area"];
30
+ readonly additionalProperties: false;
31
+ };
32
+ };
33
+ export interface FindPeersArgs {
34
+ area: string;
35
+ k?: number;
36
+ }
37
+ export declare function parseFindPeersArgs(raw: unknown): FindPeersArgs;
38
+ export declare function executeFindPeers(client: OogaApiClient, args: FindPeersArgs): Promise<{
39
+ content: Array<{
40
+ type: "text";
41
+ text: string;
42
+ }>;
43
+ isError?: boolean;
44
+ }>;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * MCP tool: find_peers (AR-367)
3
+ *
4
+ * k-nearest-neighbour peers for a UK area, by normalized signal values.
5
+ * Returns the target's geo_code + signals_used + a ranked peers[] list
6
+ * with distance and how many signal dimensions contributed.
7
+ *
8
+ * Wraps POST /v1/peers.
9
+ */
10
+ import { OogaApiError } from "../api-client.js";
11
+ import { renderPeersBlock } from "./intelligence-format.js";
12
+ export const findPeersToolName = "find_peers";
13
+ export const findPeersToolDef = {
14
+ name: findPeersToolName,
15
+ description: "Find LSOAs similar to a UK area by k-nearest-neighbour over normalized signal values. " +
16
+ "Returns the target LSOA, the signal dimensions used in the comparison, and a ranked list of peers with distance (0 = identical, 1 = maximally distant in the normalized space) and n_dims_used. " +
17
+ "Use this when the LLM needs 'areas like this one' for a specific postcode or place name.",
18
+ inputSchema: {
19
+ type: "object",
20
+ properties: {
21
+ area: {
22
+ type: "string",
23
+ description: "UK postcode (e.g. 'SW1A 1AA') or place name (e.g. 'Manchester city centre'). Max 100 characters.",
24
+ },
25
+ k: {
26
+ type: "number",
27
+ description: "Number of peers to return. Default 20, max 200.",
28
+ minimum: 1,
29
+ maximum: 200,
30
+ },
31
+ },
32
+ required: ["area"],
33
+ additionalProperties: false,
34
+ },
35
+ };
36
+ export function parseFindPeersArgs(raw) {
37
+ if (typeof raw !== "object" || raw === null) {
38
+ throw new Error("find_peers arguments must be an object");
39
+ }
40
+ const obj = raw;
41
+ const area = obj.area;
42
+ const kRaw = obj.k;
43
+ if (typeof area !== "string" || area.trim().length === 0) {
44
+ throw new Error("area must be a non-empty string");
45
+ }
46
+ if (area.length > 100) {
47
+ throw new Error("area must be 100 characters or fewer");
48
+ }
49
+ let k;
50
+ if (kRaw !== undefined) {
51
+ if (typeof kRaw !== "number" || !Number.isInteger(kRaw) || kRaw < 1 || kRaw > 200) {
52
+ throw new Error("k must be an integer between 1 and 200");
53
+ }
54
+ k = kRaw;
55
+ }
56
+ return { area: area.trim(), k };
57
+ }
58
+ export async function executeFindPeers(client, args) {
59
+ try {
60
+ const result = await client.findPeers(args.area, args.k);
61
+ const lines = [];
62
+ lines.push(`# Peers · ${args.area}`);
63
+ lines.push(`Generated at: ${result.meta.generated_at}`);
64
+ lines.push("");
65
+ lines.push(renderPeersBlock(result));
66
+ return { content: [{ type: "text", text: lines.join("\n").trimEnd() }] };
67
+ }
68
+ catch (err) {
69
+ if (err instanceof OogaApiError) {
70
+ const msg = `OneGoodArea API error (HTTP ${err.status ?? "?"}): ${err.message}`;
71
+ return { content: [{ type: "text", text: msg }], isError: true };
72
+ }
73
+ const msg = err instanceof Error ? err.message : String(err);
74
+ return { content: [{ type: "text", text: `Tool error: ${msg}` }], isError: true };
75
+ }
76
+ }
77
+ //# sourceMappingURL=find-peers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"find-peers.js","sourceRoot":"","sources":["../../src/tools/find-peers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * MCP tool: get_area_signals (AR-366)
3
+ *
4
+ * Returns the full Signals catalog for a UK area: every signal across all
5
+ * seven categories with raw value + unit, percentile when store-backed,
6
+ * per-signal confidence + engine-grounded reason, source attribution, and
7
+ * observation period. Wraps GET /v1/area.
8
+ *
9
+ * Use this when the LLM needs to inspect the underlying data — not a
10
+ * composite score, the actual signal values that would feed a score.
11
+ */
12
+ import type { OogaApiClient } from "../api-client.js";
13
+ export declare const getAreaSignalsToolName = "get_area_signals";
14
+ export declare const getAreaSignalsToolDef: {
15
+ readonly name: "get_area_signals";
16
+ readonly description: string;
17
+ readonly inputSchema: {
18
+ readonly type: "object";
19
+ readonly properties: {
20
+ readonly area: {
21
+ readonly type: "string";
22
+ readonly description: "UK postcode (e.g. 'SW1A 1AA') or place name (e.g. 'Manchester city centre', 'Shoreditch'). Max 100 characters.";
23
+ };
24
+ };
25
+ readonly required: readonly ["area"];
26
+ readonly additionalProperties: false;
27
+ };
28
+ };
29
+ export interface GetAreaSignalsArgs {
30
+ area: string;
31
+ }
32
+ export declare function parseGetAreaSignalsArgs(raw: unknown): GetAreaSignalsArgs;
33
+ export declare function executeGetAreaSignals(client: OogaApiClient, args: GetAreaSignalsArgs): Promise<{
34
+ content: Array<{
35
+ type: "text";
36
+ text: string;
37
+ }>;
38
+ isError?: boolean;
39
+ }>;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * MCP tool: get_area_signals (AR-366)
3
+ *
4
+ * Returns the full Signals catalog for a UK area: every signal across all
5
+ * seven categories with raw value + unit, percentile when store-backed,
6
+ * per-signal confidence + engine-grounded reason, source attribution, and
7
+ * observation period. Wraps GET /v1/area.
8
+ *
9
+ * Use this when the LLM needs to inspect the underlying data — not a
10
+ * composite score, the actual signal values that would feed a score.
11
+ */
12
+ import { OogaApiError } from "../api-client.js";
13
+ import { formatAreaProfileAsText } from "./signals-format.js";
14
+ export const getAreaSignalsToolName = "get_area_signals";
15
+ export const getAreaSignalsToolDef = {
16
+ name: getAreaSignalsToolName,
17
+ description: "Get the full OneGoodArea Signals catalog for a UK area (every signal across all seven categories: crime, deprivation, property, schools, amenities, transport, environment). " +
18
+ "Returns each signal's raw value with unit, percentile (when store-backed), confidence with engine-grounded reasoning, source attribution, and observation period. " +
19
+ "Use this to inspect underlying data rather than a composite score — the primitive that powers every other product.",
20
+ inputSchema: {
21
+ type: "object",
22
+ properties: {
23
+ area: {
24
+ type: "string",
25
+ description: "UK postcode (e.g. 'SW1A 1AA') or place name (e.g. 'Manchester city centre', 'Shoreditch'). Max 100 characters.",
26
+ },
27
+ },
28
+ required: ["area"],
29
+ additionalProperties: false,
30
+ },
31
+ };
32
+ export function parseGetAreaSignalsArgs(raw) {
33
+ if (typeof raw !== "object" || raw === null) {
34
+ throw new Error("get_area_signals arguments must be an object");
35
+ }
36
+ const obj = raw;
37
+ const area = obj.area;
38
+ if (typeof area !== "string" || area.trim().length === 0) {
39
+ throw new Error("area must be a non-empty string");
40
+ }
41
+ if (area.length > 100) {
42
+ throw new Error("area must be 100 characters or fewer");
43
+ }
44
+ return { area: area.trim() };
45
+ }
46
+ export async function executeGetAreaSignals(client, args) {
47
+ try {
48
+ const result = await client.getAreaSignals(args.area);
49
+ return { content: [{ type: "text", text: formatAreaProfileAsText(result) }] };
50
+ }
51
+ catch (err) {
52
+ if (err instanceof OogaApiError) {
53
+ const msg = `OneGoodArea API error (HTTP ${err.status ?? "?"}): ${err.message}`;
54
+ return { content: [{ type: "text", text: msg }], isError: true };
55
+ }
56
+ const msg = err instanceof Error ? err.message : String(err);
57
+ return { content: [{ type: "text", text: `Tool error: ${msg}` }], isError: true };
58
+ }
59
+ }
60
+ //# sourceMappingURL=get-area-signals.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-area-signals.js","sourceRoot":"","sources":["../../src/tools/get-area-signals.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAE9D,MAAM,CAAC,MAAM,sBAAsB,GAAG,kBAAkB,CAAC;AAEzD,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,IAAI,EAAE,sBAAsB;IAC5B,WAAW,EACT,+KAA+K;QAC/K,oKAAoK;QACpK,oHAAoH;IACtH,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE;YACV,IAAI,EAAE;gBACJ,IAAI,EAAE,QAAQ;gBACd,WAAW,EACT,gHAAgH;aACnH;SACF;QACD,QAAQ,EAAE,CAAC,MAAM,CAAC;QAClB,oBAAoB,EAAE,KAAK;KAC5B;CACO,CAAC;AAMX,MAAM,UAAU,uBAAuB,CAAC,GAAY;IAClD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QAC5C,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;IAClE,CAAC;IACD,MAAM,GAAG,GAAG,GAA8B,CAAC;IAC3C,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC;IAEtB,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzD,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;IACrD,CAAC;IACD,IAAI,IAAI,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC1D,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;AAC/B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,MAAqB,EACrB,IAAwB;IAExB,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtD,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,uBAAuB,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;IAChF,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,YAAY,EAAE,CAAC;YAChC,MAAM,GAAG,GAAG,+BAA+B,GAAG,CAAC,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,OAAO,EAAE,CAAC;YAChF,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QACnE,CAAC;QACD,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC7D,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,EAAE,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IACpF,CAAC;AACH,CAAC"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * MCP tool: get_portfolio_changes (AR-368)
3
+ *
4
+ * Check a Monitor portfolio for material signal changes between two
5
+ * time-series periods. Wraps POST /v1/portfolios/:id/changes (with
6
+ * emit:false — the MCP shouldn't fire webhooks on a probe call).
7
+ *
8
+ * Returns the change report rendered as markdown: scope (baseline,
9
+ * threshold, min_transactions), counts (areas_checked, material_count),
10
+ * and a per-area table of material signal moves.
11
+ */
12
+ import type { OogaApiClient, OogaChangeReport } from "../api-client.js";
13
+ export declare const getPortfolioChangesToolName = "get_portfolio_changes";
14
+ export declare const getPortfolioChangesToolDef: {
15
+ readonly name: "get_portfolio_changes";
16
+ readonly description: string;
17
+ readonly inputSchema: {
18
+ readonly type: "object";
19
+ readonly properties: {
20
+ readonly portfolio_id: {
21
+ readonly type: "string";
22
+ readonly description: "ID returned by watch_portfolio (e.g. 'ptf_...').";
23
+ };
24
+ readonly threshold_pct: {
25
+ readonly type: "number";
26
+ readonly description: "Minimum |percent change| to flag a move as material. Default is the portfolio's configured threshold. Non-negative.";
27
+ readonly minimum: 0;
28
+ };
29
+ readonly baseline: {
30
+ readonly type: "string";
31
+ readonly enum: readonly ["previous", "first"];
32
+ readonly description: "Compare the latest period vs the previous period (default), or vs the first period in range.";
33
+ };
34
+ readonly min_transactions: {
35
+ readonly type: "number";
36
+ readonly description: "Sample-size gate for price moves (de-noise). Non-negative integer; default is the portfolio's configured value.";
37
+ readonly minimum: 0;
38
+ };
39
+ };
40
+ readonly required: readonly ["portfolio_id"];
41
+ readonly additionalProperties: false;
42
+ };
43
+ };
44
+ export interface GetPortfolioChangesArgs {
45
+ portfolio_id: string;
46
+ threshold_pct?: number;
47
+ baseline?: "previous" | "first";
48
+ min_transactions?: number;
49
+ }
50
+ export declare function parseGetPortfolioChangesArgs(raw: unknown): GetPortfolioChangesArgs;
51
+ export declare function formatChangeReportAsText(report: OogaChangeReport): string;
52
+ export declare function executeGetPortfolioChanges(client: OogaApiClient, args: GetPortfolioChangesArgs): Promise<{
53
+ content: Array<{
54
+ type: "text";
55
+ text: string;
56
+ }>;
57
+ isError?: boolean;
58
+ }>;
@@ -0,0 +1,121 @@
1
+ /**
2
+ * MCP tool: get_portfolio_changes (AR-368)
3
+ *
4
+ * Check a Monitor portfolio for material signal changes between two
5
+ * time-series periods. Wraps POST /v1/portfolios/:id/changes (with
6
+ * emit:false — the MCP shouldn't fire webhooks on a probe call).
7
+ *
8
+ * Returns the change report rendered as markdown: scope (baseline,
9
+ * threshold, min_transactions), counts (areas_checked, material_count),
10
+ * and a per-area table of material signal moves.
11
+ */
12
+ import { OogaApiError } from "../api-client.js";
13
+ export const getPortfolioChangesToolName = "get_portfolio_changes";
14
+ export const getPortfolioChangesToolDef = {
15
+ name: getPortfolioChangesToolName,
16
+ description: "Check a Monitor portfolio for material signal changes between two time-series periods. " +
17
+ "Returns the portfolio's baseline + threshold settings, counts of areas checked and material changes, and a per-area table of every material signal move (with direction, from/to values, delta, and percent change). " +
18
+ "Use this after watch_portfolio has set up tracking — typically with default threshold_pct, or tighter when you want only large moves.",
19
+ inputSchema: {
20
+ type: "object",
21
+ properties: {
22
+ portfolio_id: {
23
+ type: "string",
24
+ description: "ID returned by watch_portfolio (e.g. 'ptf_...').",
25
+ },
26
+ threshold_pct: {
27
+ type: "number",
28
+ description: "Minimum |percent change| to flag a move as material. Default is the portfolio's configured threshold. Non-negative.",
29
+ minimum: 0,
30
+ },
31
+ baseline: {
32
+ type: "string",
33
+ enum: ["previous", "first"],
34
+ description: "Compare the latest period vs the previous period (default), or vs the first period in range.",
35
+ },
36
+ min_transactions: {
37
+ type: "number",
38
+ description: "Sample-size gate for price moves (de-noise). Non-negative integer; default is the portfolio's configured value.",
39
+ minimum: 0,
40
+ },
41
+ },
42
+ required: ["portfolio_id"],
43
+ additionalProperties: false,
44
+ },
45
+ };
46
+ export function parseGetPortfolioChangesArgs(raw) {
47
+ if (typeof raw !== "object" || raw === null) {
48
+ throw new Error("get_portfolio_changes arguments must be an object");
49
+ }
50
+ const obj = raw;
51
+ const portfolio_id = obj.portfolio_id;
52
+ if (typeof portfolio_id !== "string" || portfolio_id.trim().length === 0) {
53
+ throw new Error("portfolio_id must be a non-empty string");
54
+ }
55
+ const out = { portfolio_id: portfolio_id.trim() };
56
+ if (obj.threshold_pct !== undefined) {
57
+ if (typeof obj.threshold_pct !== "number" || !Number.isFinite(obj.threshold_pct) || obj.threshold_pct < 0) {
58
+ throw new Error("threshold_pct must be a non-negative number");
59
+ }
60
+ out.threshold_pct = obj.threshold_pct;
61
+ }
62
+ if (obj.baseline !== undefined) {
63
+ if (obj.baseline !== "previous" && obj.baseline !== "first") {
64
+ throw new Error("baseline must be 'previous' or 'first'");
65
+ }
66
+ out.baseline = obj.baseline;
67
+ }
68
+ if (obj.min_transactions !== undefined) {
69
+ if (typeof obj.min_transactions !== "number" || !Number.isFinite(obj.min_transactions) || obj.min_transactions < 0) {
70
+ throw new Error("min_transactions must be a non-negative number");
71
+ }
72
+ out.min_transactions = obj.min_transactions;
73
+ }
74
+ return out;
75
+ }
76
+ function formatChange(c) {
77
+ const arrow = c.direction === "up" ? "↑" : c.direction === "down" ? "↓" : "→";
78
+ const pct = c.pct_change === null ? "—" : `${c.pct_change > 0 ? "+" : ""}${c.pct_change.toFixed(1)}%`;
79
+ const from = c.value_from === null ? "—" : c.value_from.toLocaleString("en-GB");
80
+ const to = c.value_to === null ? "—" : c.value_to.toLocaleString("en-GB");
81
+ const label = c.label ?? c.signal_key;
82
+ return `| ${c.area} (${c.geo_code}) | ${label} | ${c.period_from} → ${c.period_to} | ${from} ${arrow} ${to} | ${pct} |`;
83
+ }
84
+ export function formatChangeReportAsText(report) {
85
+ const lines = [];
86
+ lines.push(`# Portfolio changes · \`${report.portfolio_id}\``);
87
+ lines.push(`Generated at: ${report.generated_at}`);
88
+ lines.push("");
89
+ lines.push(`**Baseline:** ${report.baseline}`);
90
+ lines.push(`**Threshold:** ${report.threshold_pct}% · **Min transactions:** ${report.min_transactions}`);
91
+ lines.push(`**Areas checked:** ${report.areas_checked} · **Material changes:** ${report.material_count}`);
92
+ lines.push("");
93
+ if (report.changes.length === 0) {
94
+ lines.push("No material signal changes detected for this portfolio with the current threshold.");
95
+ return lines.join("\n").trimEnd();
96
+ }
97
+ lines.push("| Area | Signal | Period | Value | % change |");
98
+ lines.push("|---|---|---|---|---|");
99
+ for (const c of report.changes)
100
+ lines.push(formatChange(c));
101
+ return lines.join("\n").trimEnd();
102
+ }
103
+ export async function executeGetPortfolioChanges(client, args) {
104
+ try {
105
+ const report = await client.getPortfolioChanges(args.portfolio_id, {
106
+ baseline: args.baseline,
107
+ threshold_pct: args.threshold_pct,
108
+ min_transactions: args.min_transactions,
109
+ });
110
+ return { content: [{ type: "text", text: formatChangeReportAsText(report) }] };
111
+ }
112
+ catch (err) {
113
+ if (err instanceof OogaApiError) {
114
+ const msg = `OneGoodArea API error (HTTP ${err.status ?? "?"}): ${err.message}`;
115
+ return { content: [{ type: "text", text: msg }], isError: true };
116
+ }
117
+ const msg = err instanceof Error ? err.message : String(err);
118
+ return { content: [{ type: "text", text: `Tool error: ${msg}` }], isError: true };
119
+ }
120
+ }
121
+ //# sourceMappingURL=get-portfolio-changes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-portfolio-changes.js","sourceRoot":"","sources":["../../src/tools/get-portfolio-changes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * MCP tool: get_signals_by_category (AR-366)
3
+ *
4
+ * Returns the Signals for one category only (crime, deprivation, property,
5
+ * schools, amenities, transport, or environment). Use this when the LLM
6
+ * needs to focus on a single data domain — narrower than get_area_signals,
7
+ * same engine-grounded fields per signal.
8
+ *
9
+ * Wraps GET /v1/signals/:category.
10
+ */
11
+ import type { OogaApiClient, SignalCategory } from "../api-client.js";
12
+ export declare const getSignalsByCategoryToolName = "get_signals_by_category";
13
+ export declare const getSignalsByCategoryToolDef: {
14
+ readonly name: "get_signals_by_category";
15
+ readonly description: string;
16
+ readonly inputSchema: {
17
+ readonly type: "object";
18
+ readonly properties: {
19
+ readonly area: {
20
+ readonly type: "string";
21
+ readonly description: "UK postcode (e.g. 'SW1A 1AA') or place name (e.g. 'Manchester city centre'). Max 100 characters.";
22
+ };
23
+ readonly category: {
24
+ readonly type: "string";
25
+ readonly enum: readonly ["crime", "deprivation", "property", "schools", "amenities", "transport", "environment"];
26
+ readonly description: "Signal category to return. One of: crime, deprivation, property, schools, amenities, transport, environment.";
27
+ };
28
+ };
29
+ readonly required: readonly ["area", "category"];
30
+ readonly additionalProperties: false;
31
+ };
32
+ };
33
+ export interface GetSignalsByCategoryArgs {
34
+ area: string;
35
+ category: SignalCategory;
36
+ }
37
+ export declare function parseGetSignalsByCategoryArgs(raw: unknown): GetSignalsByCategoryArgs;
38
+ export declare function executeGetSignalsByCategory(client: OogaApiClient, args: GetSignalsByCategoryArgs): Promise<{
39
+ content: Array<{
40
+ type: "text";
41
+ text: string;
42
+ }>;
43
+ isError?: boolean;
44
+ }>;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * MCP tool: get_signals_by_category (AR-366)
3
+ *
4
+ * Returns the Signals for one category only (crime, deprivation, property,
5
+ * schools, amenities, transport, or environment). Use this when the LLM
6
+ * needs to focus on a single data domain — narrower than get_area_signals,
7
+ * same engine-grounded fields per signal.
8
+ *
9
+ * Wraps GET /v1/signals/:category.
10
+ */
11
+ import { OogaApiError, SIGNAL_CATEGORIES } from "../api-client.js";
12
+ import { formatAreaProfileAsText } from "./signals-format.js";
13
+ export const getSignalsByCategoryToolName = "get_signals_by_category";
14
+ export const getSignalsByCategoryToolDef = {
15
+ name: getSignalsByCategoryToolName,
16
+ description: "Get OneGoodArea Signals for a single category at a UK area. Categories: crime (police.uk), deprivation (IMD/WIMD/SIMD), property (HM Land Registry), schools (Ofsted/Estyn/Education Scotland), amenities (OpenStreetMap), transport (OpenStreetMap), environment (Environment Agency). " +
17
+ "Same per-signal shape as get_area_signals — value, unit, percentile (when store-backed), confidence with engine-grounded reason, source, observation period — narrowed to one category for focused analysis.",
18
+ inputSchema: {
19
+ type: "object",
20
+ properties: {
21
+ area: {
22
+ type: "string",
23
+ description: "UK postcode (e.g. 'SW1A 1AA') or place name (e.g. 'Manchester city centre'). Max 100 characters.",
24
+ },
25
+ category: {
26
+ type: "string",
27
+ enum: SIGNAL_CATEGORIES,
28
+ description: "Signal category to return. One of: crime, deprivation, property, schools, amenities, transport, environment.",
29
+ },
30
+ },
31
+ required: ["area", "category"],
32
+ additionalProperties: false,
33
+ },
34
+ };
35
+ export function parseGetSignalsByCategoryArgs(raw) {
36
+ if (typeof raw !== "object" || raw === null) {
37
+ throw new Error("get_signals_by_category arguments must be an object");
38
+ }
39
+ const obj = raw;
40
+ const area = obj.area;
41
+ const category = obj.category;
42
+ if (typeof area !== "string" || area.trim().length === 0) {
43
+ throw new Error("area must be a non-empty string");
44
+ }
45
+ if (area.length > 100) {
46
+ throw new Error("area must be 100 characters or fewer");
47
+ }
48
+ if (typeof category !== "string" || !SIGNAL_CATEGORIES.includes(category)) {
49
+ throw new Error(`category must be one of: ${SIGNAL_CATEGORIES.join(", ")}`);
50
+ }
51
+ return { area: area.trim(), category: category };
52
+ }
53
+ export async function executeGetSignalsByCategory(client, args) {
54
+ try {
55
+ const result = await client.getSignalsByCategory(args.area, args.category);
56
+ return { content: [{ type: "text", text: formatAreaProfileAsText(result, args.category) }] };
57
+ }
58
+ catch (err) {
59
+ if (err instanceof OogaApiError) {
60
+ const msg = `OneGoodArea API error (HTTP ${err.status ?? "?"}): ${err.message}`;
61
+ return { content: [{ type: "text", text: msg }], isError: true };
62
+ }
63
+ const msg = err instanceof Error ? err.message : String(err);
64
+ return { content: [{ type: "text", text: `Tool error: ${msg}` }], isError: true };
65
+ }
66
+ }
67
+ //# sourceMappingURL=get-signals-by-category.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-signals-by-category.js","sourceRoot":"","sources":["../../src/tools/get-signals-by-category.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * AR-367: Format /v1/query responses for the find_areas MCP tool.
3
+ *
4
+ * The query plane returns a discriminated union over 7 plan ops. This
5
+ * module renders each op's results as markdown the LLM can pass to its
6
+ * user. Every value rendered is a real field from the response — no
7
+ * client-side text synthesis.
8
+ *
9
+ * Rendering bias: surface the emitted PLAN front-and-center (the
10
+ * "what did the planner decide to run" transparency play that makes
11
+ * the query plane defensible), then the op-specific results.
12
+ */
13
+ import type { OogaQueryResponse } from "../api-client.js";
14
+ export declare function renderPeersBlock(r: {
15
+ target: {
16
+ geo_code: string;
17
+ signals_used: string[];
18
+ };
19
+ peers: Array<{
20
+ geo_code: string;
21
+ distance: number;
22
+ n_dims_used: number;
23
+ }>;
24
+ meta: {
25
+ scope: string;
26
+ };
27
+ }): string;
28
+ export declare function formatQueryResponseAsText(res: OogaQueryResponse): string;