@mountainpass/addressr-mcp 0.1.0 → 0.2.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 (2) hide show
  1. package/package.json +4 -2
  2. package/src/server.mjs +61 -18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mountainpass/addressr-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "MCP server for Australian address search and validation via Addressr",
5
5
  "author": {
6
6
  "name": "Mountain Pass",
@@ -22,10 +22,12 @@
22
22
  "test": "node --test test/server.test.mjs",
23
23
  "lint": "eslint .",
24
24
  "pre-commit": "lint-staged",
25
- "push:watch": "bash scripts/push-and-watch.sh"
25
+ "push:watch": "bash scripts/push-and-watch.sh",
26
+ "release:watch": "bash scripts/release-watch.sh"
26
27
  },
27
28
  "dependencies": {
28
29
  "@modelcontextprotocol/sdk": "^1.29.0",
30
+ "@windyroad/fetch-link": "^3.1.0",
29
31
  "zod": "^4.3.0"
30
32
  },
31
33
  "devDependencies": {
package/src/server.mjs CHANGED
@@ -1,22 +1,14 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { glowUpFetchWithLinks } from '@windyroad/fetch-link';
3
4
  import { z } from 'zod';
4
5
 
6
+ const API_URL =
7
+ process.env.ADDRESSR_API_URL || 'https://addressr.p.rapidapi.com/';
5
8
  const API_HOST = process.env.RAPIDAPI_HOST || 'addressr.p.rapidapi.com';
6
- const API_BASE = `https://${API_HOST}`;
7
9
 
8
- async function apiCall(path, key) {
9
- const response = await fetch(`${API_BASE}${path}`, {
10
- headers: {
11
- 'x-rapidapi-key': key,
12
- 'x-rapidapi-host': API_HOST,
13
- },
14
- });
15
- if (!response.ok) {
16
- throw new Error(`API error: ${response.status} ${response.statusText}`);
17
- }
18
- return response.json();
19
- }
10
+ const SEARCH_REL = 'https://addressr.io/rels/address-search';
11
+ const HEALTH_REL = 'https://addressr.io/rels/health';
20
12
 
21
13
  export function createServer() {
22
14
  const key = process.env.RAPIDAPI_KEY;
@@ -27,6 +19,28 @@ export function createServer() {
27
19
  process.exit(1);
28
20
  }
29
21
 
22
+ const headers = {
23
+ 'x-rapidapi-key': key,
24
+ 'x-rapidapi-host': API_HOST,
25
+ };
26
+
27
+ // Create a fetch-link instance with RapidAPI auth headers baked in
28
+ const fetchLink = glowUpFetchWithLinks((url, init) =>
29
+ fetch(url, { ...init, headers: { ...headers, ...init?.headers } }),
30
+ );
31
+
32
+ // Cache the API root discovery (1 week cache-control)
33
+ let rootPromise;
34
+ function getRoot() {
35
+ if (!rootPromise) {
36
+ rootPromise = fetchLink(API_URL).catch((err) => {
37
+ rootPromise = undefined;
38
+ throw err;
39
+ });
40
+ }
41
+ return rootPromise;
42
+ }
43
+
30
44
  const server = new McpServer({
31
45
  name: 'addressr',
32
46
  version: '0.1.0',
@@ -47,9 +61,15 @@ export function createServer() {
47
61
  .describe('Page number for paginated results (default: first page)'),
48
62
  },
49
63
  async ({ q, page }) => {
50
- const params = new URLSearchParams({ q });
51
- if (page !== undefined) params.set('page', String(page));
52
- const data = await apiCall(`/addresses?${params}`, key);
64
+ const root = await getRoot();
65
+ const params = { q };
66
+ if (page !== undefined) params.page = String(page);
67
+ const searchLinks = root.links(SEARCH_REL, params);
68
+ if (!searchLinks.length) {
69
+ throw new Error('Search link relation not found in API root');
70
+ }
71
+ const response = await fetchLink(searchLinks[0]);
72
+ const data = await response.json();
53
73
  return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
54
74
  },
55
75
  );
@@ -65,7 +85,17 @@ export function createServer() {
65
85
  ),
66
86
  },
67
87
  async ({ addressId }) => {
68
- const data = await apiCall(`/addresses/${encodeURIComponent(addressId)}`, key);
88
+ // Follow the canonical link pattern from search results
89
+ // The search response includes canonical links: </addresses/{pid}>; rel=canonical
90
+ // We construct the same URL and follow it via fetchLink
91
+ const root = await getRoot();
92
+ const baseUrl = new URL(root.url);
93
+ const addressUrl = new URL(
94
+ `/addresses/${encodeURIComponent(addressId)}`,
95
+ baseUrl,
96
+ );
97
+ const response = await fetchLink(addressUrl.toString());
98
+ const data = await response.json();
69
99
  return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
70
100
  },
71
101
  );
@@ -75,7 +105,20 @@ export function createServer() {
75
105
  'Check API service status. Returns version, timestamp, and health status.',
76
106
  {},
77
107
  async () => {
78
- const data = await apiCall('/health', key);
108
+ const root = await getRoot();
109
+ const healthLinks = root.links(HEALTH_REL);
110
+ if (healthLinks.length && healthLinks[0].uri !== 'undefined') {
111
+ const response = await fetchLink(healthLinks[0]);
112
+ const data = await response.json();
113
+ return {
114
+ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
115
+ };
116
+ }
117
+ // Fallback: health link URI is currently broken in production (returns "undefined")
118
+ const baseUrl = new URL(root.url);
119
+ const healthUrl = new URL('/health', baseUrl);
120
+ const response = await fetchLink(healthUrl.toString());
121
+ const data = await response.json();
79
122
  return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
80
123
  },
81
124
  );