@ekoindia/eps-context-mcp 0.1.2 → 0.1.4

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 CHANGED
@@ -15,9 +15,13 @@ fetch full detail when it needs it.
15
15
  No install step required — run it on demand with `npx`:
16
16
 
17
17
  ```bash
18
- npx -y @ekoindia/eps-context-mcp
18
+ npx -y @ekoindia/eps-context-mcp@latest
19
19
  ```
20
20
 
21
+ The `@latest` tag makes `npx` re-resolve the newest published version on each
22
+ launch, so you always run the current server + API bundle without editing your
23
+ config (see [Staying up to date](#staying-up-to-date)).
24
+
21
25
  The server speaks MCP over **stdio**, so it is meant to be launched by an MCP
22
26
  client (see per-harness config below) rather than run by hand. It logs its
23
27
  status to **stderr** and waits for an MCP client on stdin/stdout.
@@ -27,38 +31,52 @@ Requires **Node.js ≥ 18**.
27
31
  ## Tools
28
32
 
29
33
  All tools are read-only and **secret-free** — none of them accept an
30
- `access_key` or any other credential.
31
-
32
- | Tool | Arguments | Returns |
33
- | --- | --- | --- |
34
- | `list_apis` | `category?` | Compact index of EPS endpoints (no request/response bodies). Optional category filter. |
35
- | `list_topics` | — | Documentation topic ids: `auth`, `errors`, `pricing`, `environments`. |
36
- | `list_recipes` | — | Multi-step recipe ids + names (e.g. `dmt-send-money`, `aeps-cash-withdrawal`). |
37
- | `search` | `query` | Ranked endpoint matches for a query (ids only, no bodies). |
38
- | `get_api` | `slug` | Full detail for one endpoint (params, response fields, errors, examples). |
39
- | `get_topic` | `topic` (`auth` \| `errors` \| `pricing` \| `environments`) | One documentation topic. |
40
- | `get_recipe` | `id` | One multi-step recipe (steps + branches). |
41
- | `get_signing_snippet` | `language` (`php` \| `java` \| `csharp` \| `javascript` \| `python` \| `go`) | Paste-ready **backend** code to compute the request `secret-key`. |
42
- | `get_meta` | — | Bundle org/version + which data source is in use (`baked` or `remote`). |
34
+ `access_key` or any other credential. Every tool also declares MCP annotations
35
+ (`readOnlyHint: true`, `idempotentHint: true`, `openWorldHint: false`) so
36
+ clients can see this programmatically.
37
+
38
+ | Tool | Arguments | Returns |
39
+ | --------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
40
+ | `list_apis` | `category?`, `limit?` | Compact index of EPS endpoints (no request/response bodies). `category` is validated against the bundle's categories; all entries by default. |
41
+ | `list_topics` | — | Documentation topic ids: `auth`, `errors`, `pricing`, `environments`. |
42
+ | `list_recipes` | — | Multi-step recipe ids + names (e.g. `dmt-send-money`, `aeps-cash-withdrawal`). |
43
+ | `search` | `query`, `limit?` | Ranked endpoint matches for a query (ids only, no bodies). Top 10 by default; raise `limit` for more. |
44
+ | `get_api` | `slug` | Full detail for one endpoint (params, response fields, errors, examples). |
45
+ | `get_topic` | `topic` (`auth` \| `errors` \| `pricing` \| `environments`) | One documentation topic. |
46
+ | `get_recipe` | `id` | One multi-step recipe (steps + branches). |
47
+ | `get_signing_snippet` | `language` (`php` \| `java` \| `csharp` \| `javascript` \| `python` \| `go`) | Paste-ready **backend** code to compute the request `secret-key`. |
48
+ | `get_meta` | — | Bundle org/version, data source (`baked` or `remote`), this server's `packageVersion`, and `updateAvailable` (whether a newer npm release exists). |
43
49
 
44
50
  **Tiered usage:** start with `list_apis` / `search` (cheap, compact), then call
45
51
  `get_api` only for the endpoint(s) you actually need. Same pattern for
46
52
  `list_topics` → `get_topic` and `list_recipes` → `get_recipe`.
47
53
 
54
+ **Errors are actionable:** an unknown `slug`/`id` returns an MCP error result
55
+ (`isError: true`) with "did you mean" suggestions and a pointer to
56
+ `search`/`list_apis`; an invalid `category` fails schema validation with the
57
+ valid values listed.
58
+
48
59
  ## Client configuration
49
60
 
50
61
  ### Claude Code
51
62
 
52
63
  ```bash
53
- claude mcp add eps -- npx -y @ekoindia/eps-context-mcp
64
+ claude mcp add eps --scope project -- npx -y @ekoindia/eps-context-mcp@latest
54
65
  ```
55
66
 
56
- Or add to your MCP config (`~/.claude.json` / project `.mcp.json`):
67
+ `--scope project` writes a shared `.mcp.json` committed with the repo. Use
68
+ `--scope user` for every project on this machine, or drop the flag for local
69
+ scope (private to this checkout, not shared).
70
+
71
+ Or add to your MCP config (project `.mcp.json`, or `~/.claude.json` for user scope):
57
72
 
58
73
  ```json
59
74
  {
60
75
  "mcpServers": {
61
- "eps": { "command": "npx", "args": ["-y", "@ekoindia/eps-context-mcp"] }
76
+ "eps": {
77
+ "command": "npx",
78
+ "args": ["-y", "@ekoindia/eps-context-mcp@latest"]
79
+ }
62
80
  }
63
81
  }
64
82
  ```
@@ -70,7 +88,10 @@ Or add to your MCP config (`~/.claude.json` / project `.mcp.json`):
70
88
  ```json
71
89
  {
72
90
  "mcpServers": {
73
- "eps": { "command": "npx", "args": ["-y", "@ekoindia/eps-context-mcp"] }
91
+ "eps": {
92
+ "command": "npx",
93
+ "args": ["-y", "@ekoindia/eps-context-mcp@latest"]
94
+ }
74
95
  }
75
96
  }
76
97
  ```
@@ -84,7 +105,7 @@ Or add to your MCP config (`~/.claude.json` / project `.mcp.json`):
84
105
  "mcp": {
85
106
  "eps": {
86
107
  "type": "local",
87
- "command": ["npx", "-y", "@ekoindia/eps-context-mcp"],
108
+ "command": ["npx", "-y", "@ekoindia/eps-context-mcp@latest"],
88
109
  "enabled": true
89
110
  }
90
111
  }
@@ -99,7 +120,7 @@ Or add to your MCP config (`~/.claude.json` / project `.mcp.json`):
99
120
  mcpServers:
100
121
  - name: eps
101
122
  command: npx
102
- args: ["-y", "@ekoindia/eps-context-mcp"]
123
+ args: ["-y", "@ekoindia/eps-context-mcp@latest"]
103
124
  ```
104
125
 
105
126
  ### Codex CLI
@@ -109,7 +130,7 @@ mcpServers:
109
130
  ```toml
110
131
  [mcp_servers.eps]
111
132
  command = "npx"
112
- args = ["-y", "@ekoindia/eps-context-mcp"]
133
+ args = ["-y", "@ekoindia/eps-context-mcp@latest"]
113
134
  ```
114
135
 
115
136
  ### Gemini CLI
@@ -119,11 +140,30 @@ args = ["-y", "@ekoindia/eps-context-mcp"]
119
140
  ```json
120
141
  {
121
142
  "mcpServers": {
122
- "eps": { "command": "npx", "args": ["-y", "@ekoindia/eps-context-mcp"] }
143
+ "eps": {
144
+ "command": "npx",
145
+ "args": ["-y", "@ekoindia/eps-context-mcp@latest"]
146
+ }
123
147
  }
124
148
  }
125
149
  ```
126
150
 
151
+ ## Staying up to date
152
+
153
+ The package is auto-published on every merge to `main`, and each publish carries
154
+ the freshest baked API bundle. To keep users current:
155
+
156
+ - **Use `@latest` in your config** (all snippets above do). `npx` then
157
+ re-resolves the newest publish on each launch — no manual updates. Offline
158
+ launches fall back to the npx cache; pin `@<version>` if you need a frozen
159
+ build.
160
+ - **Update check.** On startup the server does one best-effort request to the
161
+ npm registry (3s timeout). If a newer version is published, it logs a nudge to
162
+ **stderr** and sets `updateAvailable` on `get_meta` — so an agent can tell you
163
+ to switch to `@latest`. The check is silent on any failure (offline, proxy)
164
+ and never blocks startup. Set `EPS_NO_UPDATE_CHECK=1` to disable it entirely
165
+ (no network request).
166
+
127
167
  ## Configuration
128
168
 
129
169
  ### `EPS_BUNDLE_URL` (optional)
@@ -136,16 +176,16 @@ bundle at startup (e.g. the latest generated `eps.json`), set `EPS_BUNDLE_URL`:
136
176
  "mcpServers": {
137
177
  "eps": {
138
178
  "command": "npx",
139
- "args": ["-y", "@ekoindia/eps-context-mcp"],
179
+ "args": ["-y", "@ekoindia/eps-context-mcp@latest"],
140
180
  "env": { "EPS_BUNDLE_URL": "https://eps.eko.in/agent/eps.json" }
141
181
  }
142
182
  }
143
183
  }
144
184
  ```
145
185
 
146
- If the fetch fails for any reason, the server transparently falls back to the
147
- baked bundle. `get_meta` reports which source is in effect (`baked` or
148
- `remote`).
186
+ The remote fetch is capped at **5 seconds**; if it fails or times out for any
187
+ reason, the server transparently falls back to the baked bundle. `get_meta`
188
+ reports which source is in effect (`baked` or `remote`).
149
189
 
150
190
  ## Security: backend-only signing
151
191
 
package/data/eps.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "meta": {
3
3
  "org": "ekoindia",
4
4
  "apiVersion": "v3",
5
- "bundleVersion": "2e8027ce",
5
+ "bundleVersion": "1e83ecd3",
6
6
  "environments": [
7
7
  {
8
8
  "id": "sandbox",
@@ -1856,8 +1856,8 @@
1856
1856
  "in": "header",
1857
1857
  "type": "string",
1858
1858
  "required": true,
1859
- "description": "application/json",
1860
- "example": "application/json"
1859
+ "description": "multipart/form-data — let your HTTP client set this header itself (it generates the required boundary); do not hardcode the value.",
1860
+ "example": "multipart/form-data"
1861
1861
  }
1862
1862
  ],
1863
1863
  "requestParams": [
@@ -1921,7 +1921,7 @@
1921
1921
  },
1922
1922
  {
1923
1923
  "name": "pan_card",
1924
- "type": "string",
1924
+ "type": "file",
1925
1925
  "required": true,
1926
1926
  "description": "PAN card document upload (multipart/form-data). Accepted formats: JPEG, JPG, PDF. Max size: 1 MB. PNG not accepted.",
1927
1927
  "example": "<binary file>",
@@ -1929,7 +1929,7 @@
1929
1929
  },
1930
1930
  {
1931
1931
  "name": "aadhar_front",
1932
- "type": "string",
1932
+ "type": "file",
1933
1933
  "required": true,
1934
1934
  "description": "Front side of the Aadhaar card (multipart/form-data). Accepted formats: JPEG, JPG, PDF. Max size: 1 MB.",
1935
1935
  "example": "<binary file>",
@@ -1937,7 +1937,7 @@
1937
1937
  },
1938
1938
  {
1939
1939
  "name": "aadhar_back",
1940
- "type": "string",
1940
+ "type": "file",
1941
1941
  "required": true,
1942
1942
  "description": "Back side of the Aadhaar card (multipart/form-data). Accepted formats: JPEG, JPG, PDF. Max size: 1 MB.",
1943
1943
  "example": "<binary file>",
@@ -4901,7 +4901,7 @@
4901
4901
  "status": 0,
4902
4902
  "response_status_id": 0,
4903
4903
  "message": "Bill payment successful",
4904
- "response_type_id": 2,
4904
+ "response_type_id": 333,
4905
4905
  "tx_status": "0",
4906
4906
  "txstatus_desc": "Success",
4907
4907
  "data": {
@@ -9673,11 +9673,11 @@
9673
9673
  "name": "Get Aadhaar KYC Consent Languages",
9674
9674
  "method": "GET",
9675
9675
  "path": "/customer/payment/ppi-digikhata/sender/{customer_id}/aadhaar/consent/languages",
9676
- "summary": "List the languages available for the DigiKhata Aadhaar e-KYC consent.",
9676
+ "summary": "List the languages available for the DigiKhata Aadhaar eKYC consent.",
9677
9677
  "category": "bc",
9678
9678
  "relevance": "L",
9679
- "description": "Returns the supported consent languages (with their `pkid`) for DigiKhata Aadhaar e-KYC. Pass the chosen `pkid` as `consent_language` to Get Aadhaar KYC Consent Details.",
9680
- "bestFor": "Presenting Aadhaar e-KYC consent in the customer's language.",
9679
+ "description": "Returns the supported consent languages (with their `pkid`) for DigiKhata Aadhaar eKYC. Pass the chosen `pkid` as `consent_language` to Get Aadhaar KYC Consent Details.",
9680
+ "bestFor": "Presenting Aadhaar eKYC consent in the customer's language.",
9681
9681
  "docsUrl": "https://eps.eko.in/docs/ppi-digikhata-consent-languages",
9682
9682
  "headers": [
9683
9683
  {
@@ -9870,11 +9870,11 @@
9870
9870
  "name": "Get Aadhaar KYC Consent Details",
9871
9871
  "method": "GET",
9872
9872
  "path": "/customer/payment/ppi-digikhata/sender/{customer_id}/aadhaar/consent/details",
9873
- "summary": "Fetch the DigiKhata Aadhaar e-KYC consent text and audio for a chosen language.",
9873
+ "summary": "Fetch the DigiKhata Aadhaar eKYC consent text and audio for a chosen language.",
9874
9874
  "category": "bc",
9875
9875
  "relevance": "L",
9876
- "description": "Returns the full Aadhaar e-KYC consent content, short consent statement, and an audio URL for the language selected (via `consent_language` = the `pkid` from Get Aadhaar KYC Consent Languages). Display/play this consent before collecting Aadhaar OTP.",
9877
- "bestFor": "Showing the mandatory Aadhaar e-KYC consent before OTP capture.",
9876
+ "description": "Returns the full Aadhaar eKYC consent content, short consent statement, and an audio URL for the language selected (via `consent_language` = the `pkid` from Get Aadhaar KYC Consent Languages). Display/play this consent before collecting Aadhaar OTP.",
9877
+ "bestFor": "Showing the mandatory Aadhaar eKYC consent before OTP capture.",
9878
9878
  "docsUrl": "https://eps.eko.in/docs/ppi-digikhata-consent-details",
9879
9879
  "headers": [
9880
9880
  {
@@ -9989,7 +9989,7 @@
9989
9989
  {
9990
9990
  "name": "consentContent",
9991
9991
  "type": "string",
9992
- "description": "Full Aadhaar e-KYC consent text.",
9992
+ "description": "Full Aadhaar eKYC consent text.",
9993
9993
  "example": "Use my Aadhaar / Virtual ID details …"
9994
9994
  },
9995
9995
  {
@@ -10026,10 +10026,10 @@
10026
10026
  "response_status_id": 0,
10027
10027
  "data": {
10028
10028
  "consent_detail": {
10029
- "consentContent": "Use my Aadhaar / Virtual ID details (as applicable) for the purpose of e-KYC for/with PayPoint India to authenticate my identity through the Aadhaar Authentication system (Aadhaar based e-KYC services of UIDAI) in accordance with the provisions of the Aadhaar (Targeted Delivery of Financial and other Subsidies, Benefits and Services) Act, 2016 and the allied rules and regulations notified thereunder and for no other purpose.\r\n Authenticate my Aadhaar/Virtual ID through OTP or Biometric for authenticating my identity through the Aadhaar Authentication system for obtaining my e-KYC through Aadhaar based e-KYC services of UIDAI and use my Photo and Demographic details (Name, Gender, Date of Birth and Address) for the purpose of e-KYC for/with PayPoint India.\r\n I understand that Security and confidentiality of personal identity data provided, for the purpose of Aadhaar based authentication is ensured by PayPoint and the data will be stored by PayPoint till such time as mentioned in guidelines from UIDAI from time to time.",
10029
+ "consentContent": "Use my Aadhaar / Virtual ID details (as applicable) for the purpose of eKYC for/with PayPoint India to authenticate my identity through the Aadhaar Authentication system (Aadhaar based eKYC services of UIDAI) in accordance with the provisions of the Aadhaar (Targeted Delivery of Financial and other Subsidies, Benefits and Services) Act, 2016 and the allied rules and regulations notified thereunder and for no other purpose.\r\n Authenticate my Aadhaar/Virtual ID through OTP or Biometric for authenticating my identity through the Aadhaar Authentication system for obtaining my eKYC through Aadhaar based eKYC services of UIDAI and use my Photo and Demographic details (Name, Gender, Date of Birth and Address) for the purpose of eKYC for/with PayPoint India.\r\n I understand that Security and confidentiality of personal identity data provided, for the purpose of Aadhaar based authentication is ensured by PayPoint and the data will be stored by PayPoint till such time as mentioned in guidelines from UIDAI from time to time.",
10030
10030
  "audioUrl": "https://paypointindia.co.in/audio/Consent_English.mp3",
10031
10031
  "consentId": 1,
10032
- "consent": "Consent for Authentication: I, the holder of Aadhaar number, hereby give my consent to Paypoint India Network Private Limited to perform authentication and obtain my e-KYC with UIDAI for the purpose of creating my wallet.",
10032
+ "consent": "Consent for Authentication: I, the holder of Aadhaar number, hereby give my consent to Paypoint India Network Private Limited to perform authentication and obtain my eKYC with UIDAI for the purpose of creating my wallet.",
10033
10033
  "consentLanguage": "English"
10034
10034
  }
10035
10035
  },
@@ -10049,8 +10049,8 @@
10049
10049
  "summary": "Validate a DigiKhata sender's Aadhaar number and trigger an OTP to the linked mobile.",
10050
10050
  "category": "bc",
10051
10051
  "relevance": "M",
10052
- "description": "Submits the sender's Aadhaar number and dispatches an OTP to the Aadhaar-linked mobile for e-KYC. Returns an `otp_ref_id` to pass into Validate Sender Aadhaar OTP.",
10053
- "bestFor": "Starting DigiKhata Aadhaar e-KYC for a sender.",
10052
+ "description": "Submits the sender's Aadhaar number and dispatches an OTP to the Aadhaar-linked mobile for eKYC. Returns an `otp_ref_id` to pass into Validate Sender Aadhaar OTP.",
10053
+ "bestFor": "Starting DigiKhata Aadhaar eKYC for a sender.",
10054
10054
  "docsUrl": "https://eps.eko.in/docs/ppi-digikhata-generate-aadhaar-otp",
10055
10055
  "headers": [
10056
10056
  {
@@ -10188,11 +10188,11 @@
10188
10188
  "name": "Validate Sender Aadhaar OTP",
10189
10189
  "method": "POST",
10190
10190
  "path": "/customer/payment/ppi-digikhata/sender/{customer_id}/aadhaar/otp/verify",
10191
- "summary": "Verify the Aadhaar OTP to complete DigiKhata sender Aadhaar e-KYC.",
10191
+ "summary": "Verify the Aadhaar OTP to complete DigiKhata sender Aadhaar eKYC.",
10192
10192
  "category": "bc",
10193
10193
  "relevance": "M",
10194
10194
  "description": "Validates the OTP dispatched during Generate Sender Aadhaar OTP. On success the Aadhaar identity is confirmed and the flow proceeds to PAN validation (Pan Number Required).",
10195
- "bestFor": "Completing DigiKhata sender Aadhaar e-KYC.",
10195
+ "bestFor": "Completing DigiKhata sender Aadhaar eKYC.",
10196
10196
  "docsUrl": "https://eps.eko.in/docs/ppi-digikhata-verify-aadhaar-otp",
10197
10197
  "headers": [
10198
10198
  {
@@ -14324,9 +14324,9 @@
14324
14324
  },
14325
14325
  {
14326
14326
  "name": "value",
14327
- "type": "string",
14327
+ "type": "number",
14328
14328
  "description": "Parameter value.",
14329
- "example": "400"
14329
+ "example": 400
14330
14330
  }
14331
14331
  ]
14332
14332
  }
package/dist/index.js CHANGED
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
+ import { readFileSync } from "fs";
5
+ import path2 from "path";
6
+ import { fileURLToPath as fileURLToPath2 } from "url";
4
7
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
8
 
6
9
  // src/load-bundle.ts
@@ -13,7 +16,7 @@ var loadBundle = async () => {
13
16
  const url = process.env.EPS_BUNDLE_URL;
14
17
  if (url) {
15
18
  try {
16
- const res = await fetch(url);
19
+ const res = await fetch(url, { signal: AbortSignal.timeout(5e3) });
17
20
  if (res.ok)
18
21
  return { bundle: await res.json(), source: "remote" };
19
22
  } catch {
@@ -39,19 +42,26 @@ var toIndexEntry = (a) => ({
39
42
  category: a.category,
40
43
  relevance: a.relevance
41
44
  });
42
- var listApis = (bundle, category) => bundle.apis.filter((a) => !category || a.category === category).map(toIndexEntry);
45
+ var listApis = (bundle, category, limit) => {
46
+ const entries = bundle.apis.filter((a) => !category || a.category === category).map(toIndexEntry);
47
+ return limit !== void 0 ? entries.slice(0, limit) : entries;
48
+ };
49
+ var listCategories = (bundle) => [
50
+ ...new Set(bundle.apis.map((a) => a.category))
51
+ ];
43
52
  var listTopics = (bundle) => Object.keys(bundle.topics);
44
53
  var listRecipes = (bundle) => bundle.recipes.map((r) => ({ id: r.id, name: r.name, summary: r.summary }));
45
- var searchApis = (bundle, query) => {
54
+ var searchApis = (bundle, query, limit) => {
46
55
  const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
47
- if (!terms.length) return listApis(bundle);
56
+ if (!terms.length) return listApis(bundle, void 0, limit);
48
57
  const scored = bundle.apis.map((a) => {
49
58
  const hay = `${a.name} ${a.summary} ${a.path} ${a.category} ${a.productName}`.toLowerCase();
50
59
  let score = 0;
51
60
  for (const t of terms) if (hay.includes(t)) score += 1;
52
61
  return { a, score };
53
62
  });
54
- return scored.filter((s) => s.score > 0).sort((x, y) => y.score - x.score).map((s) => toIndexEntry(s.a));
63
+ const ranked = scored.filter((s) => s.score > 0).sort((x, y) => y.score - x.score).map((s) => toIndexEntry(s.a));
64
+ return limit !== void 0 ? ranked.slice(0, limit) : ranked;
55
65
  };
56
66
  var getApi = (bundle, slug) => bundle.apis.find((a) => a.slug === slug);
57
67
  var getTopic = (bundle, topic) => bundle.topics[topic];
@@ -146,28 +156,46 @@ var getSigningSnippet = (language) => SNIPPETS[language] ?? `Unsupported languag
146
156
 
147
157
  // src/server.ts
148
158
  var json = (value) => ({
149
- content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
159
+ content: [{ type: "text", text: JSON.stringify(value) }]
150
160
  });
151
- var createEpsServer = (bundle, source) => {
161
+ var notFound = (message) => ({
162
+ isError: true,
163
+ content: [{ type: "text", text: message }]
164
+ });
165
+ var READ_ONLY = {
166
+ readOnlyHint: true,
167
+ idempotentHint: true,
168
+ openWorldHint: false
169
+ };
170
+ var DEFAULT_SEARCH_LIMIT = 10;
171
+ var createEpsServer = (bundle, source, versionState) => {
152
172
  const server = new McpServer({
153
173
  name: "eps-context-mcp",
154
174
  version: bundle.meta.bundleVersion
155
175
  });
176
+ const categories = listCategories(bundle);
177
+ const categorySchema = categories.length ? z.enum(categories) : z.string();
178
+ const limitSchema = z.number().int().positive().optional();
156
179
  server.registerTool(
157
180
  "list_apis",
158
181
  {
159
182
  title: "List EPS APIs",
160
- description: "Compact index of EPS API endpoints (no request/response bodies). Optionally filter by category.",
161
- inputSchema: { category: z.string().optional() }
183
+ description: "Compact index of EPS API endpoints (no request/response bodies). Unfiltered, all ~99 entries are returned (the full tiered index); narrow with category and/or limit when you don't need everything.",
184
+ inputSchema: {
185
+ category: categorySchema.optional().describe(`One of: ${categories.join(", ")}`),
186
+ limit: limitSchema.describe("Max entries to return (default: all)")
187
+ },
188
+ annotations: READ_ONLY
162
189
  },
163
- async ({ category }) => json(listApis(bundle, category))
190
+ async ({ category, limit }) => json(listApis(bundle, category, limit))
164
191
  );
165
192
  server.registerTool(
166
193
  "list_topics",
167
194
  {
168
195
  title: "List topics",
169
196
  description: "List documentation topic ids.",
170
- inputSchema: {}
197
+ inputSchema: {},
198
+ annotations: READ_ONLY
171
199
  },
172
200
  async () => json(listTopics(bundle))
173
201
  );
@@ -176,7 +204,8 @@ var createEpsServer = (bundle, source) => {
176
204
  {
177
205
  title: "List recipes",
178
206
  description: "List multi-step recipe ids + names.",
179
- inputSchema: {}
207
+ inputSchema: {},
208
+ annotations: READ_ONLY
180
209
  },
181
210
  async () => json(listRecipes(bundle))
182
211
  );
@@ -184,21 +213,34 @@ var createEpsServer = (bundle, source) => {
184
213
  "search",
185
214
  {
186
215
  title: "Search APIs",
187
- description: "Ranked endpoint matches for a query (ids only, no bodies).",
188
- inputSchema: { query: z.string() }
216
+ description: `Ranked endpoint matches for a query (ids only, no bodies). Returns the top ${DEFAULT_SEARCH_LIMIT} by default; raise limit for more.`,
217
+ inputSchema: {
218
+ query: z.string(),
219
+ limit: limitSchema.describe(
220
+ `Max results (default ${DEFAULT_SEARCH_LIMIT})`
221
+ )
222
+ },
223
+ annotations: READ_ONLY
189
224
  },
190
- async ({ query }) => json(searchApis(bundle, query))
225
+ async ({ query, limit }) => json(searchApis(bundle, query, limit ?? DEFAULT_SEARCH_LIMIT))
191
226
  );
192
227
  server.registerTool(
193
228
  "get_api",
194
229
  {
195
230
  title: "Get API detail",
196
231
  description: "Full detail for one endpoint by slug.",
197
- inputSchema: { slug: z.string() }
232
+ inputSchema: { slug: z.string() },
233
+ annotations: READ_ONLY
198
234
  },
199
235
  async ({ slug }) => {
200
236
  const api = getApi(bundle, slug);
201
- return api ? json(api) : json({ error: `Unknown slug "${slug}"` });
237
+ if (api) return json(api);
238
+ const suggestions = searchApis(bundle, slug.replace(/[-_]/g, " "), 3).map(
239
+ (a) => a.slug
240
+ );
241
+ return notFound(
242
+ `Unknown slug "${slug}".` + (suggestions.length ? ` Did you mean: ${suggestions.join(", ")}?` : "") + ` Use search or list_apis to find valid slugs.`
243
+ );
202
244
  }
203
245
  );
204
246
  server.registerTool(
@@ -208,7 +250,8 @@ var createEpsServer = (bundle, source) => {
208
250
  description: "One topic: auth | errors | pricing | environments.",
209
251
  inputSchema: {
210
252
  topic: z.enum(["auth", "errors", "pricing", "environments"])
211
- }
253
+ },
254
+ annotations: READ_ONLY
212
255
  },
213
256
  async ({ topic }) => json(getTopic(bundle, topic))
214
257
  );
@@ -217,11 +260,16 @@ var createEpsServer = (bundle, source) => {
217
260
  {
218
261
  title: "Get recipe",
219
262
  description: "One multi-step recipe (steps + branches) by id.",
220
- inputSchema: { id: z.string() }
263
+ inputSchema: { id: z.string() },
264
+ annotations: READ_ONLY
221
265
  },
222
266
  async ({ id }) => {
223
267
  const recipe = getRecipe(bundle, id);
224
- return recipe ? json(recipe) : json({ error: `Unknown recipe "${id}"` });
268
+ if (recipe) return json(recipe);
269
+ const valid = listRecipes(bundle).map((r) => r.id).join(", ");
270
+ return notFound(
271
+ `Unknown recipe "${id}". Valid recipe ids: ${valid}. Use list_recipes for details.`
272
+ );
225
273
  }
226
274
  );
227
275
  server.registerTool(
@@ -229,7 +277,8 @@ var createEpsServer = (bundle, source) => {
229
277
  {
230
278
  title: "Get signing snippet",
231
279
  description: "Paste-ready BACKEND code to compute the secret-key. Secret-free: access_key comes from your secret store.",
232
- inputSchema: { language: z.enum(SIGNING_LANGUAGES) }
280
+ inputSchema: { language: z.enum(SIGNING_LANGUAGES) },
281
+ annotations: READ_ONLY
233
282
  },
234
283
  async ({ language }) => ({
235
284
  content: [{ type: "text", text: getSigningSnippet(language) }]
@@ -239,18 +288,75 @@ var createEpsServer = (bundle, source) => {
239
288
  "get_meta",
240
289
  {
241
290
  title: "Get meta",
242
- description: "Bundle org/version + data source.",
243
- inputSchema: {}
291
+ description: "Bundle org/version + data source, plus this server's package version and whether a newer npm release is available (updateAvailable). If an update is available, tell the user to run this server via `npx -y @ekoindia/eps-context-mcp@latest`.",
292
+ inputSchema: {},
293
+ annotations: READ_ONLY
244
294
  },
245
- async () => json({ ...bundle.meta, source })
295
+ async () => json({
296
+ ...bundle.meta,
297
+ source,
298
+ packageVersion: versionState?.current,
299
+ latestVersion: versionState?.latest,
300
+ updateAvailable: versionState?.updateAvailable
301
+ })
246
302
  );
247
303
  return server;
248
304
  };
249
305
 
306
+ // src/update-check.ts
307
+ var REGISTRY_URL = "https://registry.npmjs.org/@ekoindia/eps-context-mcp/latest";
308
+ var compareVersions = (a, b) => {
309
+ const parse = (v) => {
310
+ const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(v.trim());
311
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
312
+ };
313
+ const pa = parse(a);
314
+ const pb = parse(b);
315
+ if (!pa || !pb) return null;
316
+ for (let i = 0; i < 3; i++) {
317
+ if (pa[i] !== pb[i]) return pa[i] > pb[i] ? 1 : -1;
318
+ }
319
+ return 0;
320
+ };
321
+ var checkForUpdate = async (state) => {
322
+ if (process.env.EPS_NO_UPDATE_CHECK) return;
323
+ try {
324
+ const res = await fetch(REGISTRY_URL, {
325
+ signal: AbortSignal.timeout(3e3)
326
+ });
327
+ if (!res.ok) return;
328
+ const data = await res.json();
329
+ if (typeof data.version !== "string") return;
330
+ const cmp = compareVersions(data.version, state.current);
331
+ if (cmp === null) return;
332
+ state.latest = data.version;
333
+ state.updateAvailable = cmp > 0;
334
+ if (state.updateAvailable) {
335
+ console.error(
336
+ `eps-context-mcp ${data.version} available (running ${state.current}). Use "npx -y @ekoindia/eps-context-mcp@latest" in your MCP config to always run the newest version.`
337
+ );
338
+ }
339
+ } catch {
340
+ }
341
+ };
342
+
250
343
  // src/index.ts
344
+ function readPackageVersion() {
345
+ try {
346
+ const here2 = path2.dirname(fileURLToPath2(import.meta.url));
347
+ const pkg = JSON.parse(
348
+ readFileSync(path2.resolve(here2, "../package.json"), "utf8")
349
+ );
350
+ return pkg.version ?? "0.0.0";
351
+ } catch {
352
+ return "0.0.0";
353
+ }
354
+ }
251
355
  async function main() {
252
356
  const { bundle, source } = await loadBundle();
253
- const server = createEpsServer(bundle, source);
357
+ const versionState = { current: readPackageVersion() };
358
+ void checkForUpdate(versionState);
359
+ const server = createEpsServer(bundle, source, versionState);
254
360
  const transport = new StdioServerTransport();
255
361
  await server.connect(transport);
256
362
  console.error(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekoindia/eps-context-mcp",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Local MCP server giving AI coding agents context for Eko Platform Services (EPS) APIs.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,11 +23,12 @@
23
23
  "bake": "node scripts/bake-bundle.mjs",
24
24
  "build": "tsup",
25
25
  "test": "vitest run",
26
+ "typecheck": "tsc --noEmit",
26
27
  "prepublishOnly": "npm run bake && npm run build"
27
28
  },
28
29
  "dependencies": {
29
30
  "@modelcontextprotocol/sdk": "^1.0.0",
30
- "zod": "^3.25.76"
31
+ "zod": "^4.0.0"
31
32
  },
32
33
  "devDependencies": {
33
34
  "tsup": "^8.0.0",