@ekoindia/eps-context-mcp 0.1.13 → 0.1.15

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
@@ -45,6 +45,7 @@ clients can see this programmatically.
45
45
  | `get_topic` | `topic` (`auth` \| `errors` \| `pricing` \| `environments`) | One documentation topic. |
46
46
  | `get_recipe` | `id` | One multi-step recipe (steps + branches). |
47
47
  | `get_signing_snippet` | `language` (`php` \| `java` \| `csharp` \| `javascript` \| `python` \| `go`) | Paste-ready **backend** code to compute the request `secret-key`. |
48
+ | `debug_auth` | `timestamp?`, `secret_key?` | Diagnose a `403`: returns a known-answer test vector to check your signing against, mechanical checks on a supplied timestamp/signature, and ranked causes. **Never takes an `access_key`.** |
48
49
  | `get_meta` | — | Bundle org/version, data source (`baked` or `remote`), this server's `packageVersion`, and `updateAvailable` (whether a newer npm release exists). |
49
50
 
50
51
  **Tiered usage:** start with `list_apis` / `search` (cheap, compact), then call
@@ -206,6 +207,25 @@ languages. **This code is backend-only.**
206
207
  key. This MCP server holds **no credentials** and performs **no signing or API
207
208
  calls** itself; it only provides context and code.
208
209
 
210
+ ### Debugging a 403 without handing over a key
211
+
212
+ `debug_auth` is **secret-free by design** and there is no plan to add an
213
+ `access_key` parameter. Two reasons, both structural:
214
+
215
+ 1. This server is also reachable **anonymously over HTTP**, so a key in a tool
216
+ argument would travel to infrastructure that deliberately holds no secrets.
217
+ 2. A tool argument lands in the **caller's model context** and its transcript.
218
+ Today an agent writes `process.env.EKO_ACCESS_KEY` and never sees the value;
219
+ a signing tool would teach it to read the secret out and paste it into a chat.
220
+
221
+ Instead the tool hands back a **known-answer test vector** — a dummy key, a
222
+ fixed timestamp, and the signature they must produce. Reproduce it with your own
223
+ code and the algorithm is proven; the 403 is then almost always provisioning
224
+ (IP allowlist, inactive key, wrong environment), which `ranked_causes` walks in
225
+ likelihood order. If you want a server that signs with real credentials, that is
226
+ `@ekoindia/eps-transact-mcp`, which takes them from the environment or per-request
227
+ headers — never as tool arguments.
228
+
209
229
  ## License
210
230
 
211
231
  MIT
package/data/eps.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "meta": {
3
3
  "org": "ekoindia",
4
4
  "apiVersion": "v3",
5
- "bundleVersion": "c9e5f932",
5
+ "bundleVersion": "3f05bd61",
6
6
  "environments": [
7
7
  {
8
8
  "id": "sandbox",
@@ -70,7 +70,12 @@
70
70
  "Generate the current timestamp in milliseconds (as a string).",
71
71
  "Compute HMAC-SHA256 of the timestamp using the base64-encoded key.",
72
72
  "Base64-encode the resulting signature — this is the secret-key."
73
- ]
73
+ ],
74
+ "testVector": {
75
+ "accessKey": "test-access-key-123",
76
+ "timestamp": "1700000000000",
77
+ "secretKey": "88lqTf9ew69XbVbeczjxVL8/B4vibfp1MvTi1mIj2Xo="
78
+ }
74
79
  },
75
80
  "errors": {
76
81
  "id": "errors",
@@ -7080,6 +7085,177 @@
7080
7085
  ],
7081
7086
  "responseTypes": []
7082
7087
  },
7088
+ {
7089
+ "slug": "pan-fetch",
7090
+ "productId": "pan",
7091
+ "productName": "PAN Verification",
7092
+ "name": "Fetch PAN Details",
7093
+ "method": "POST",
7094
+ "path": "/tools/kyc/fetch-pan",
7095
+ "summary": "Fetch the PAN holder's registered full name and category from the PAN number alone.",
7096
+ "category": "verification",
7097
+ "relevance": "H",
7098
+ "description": "Fetch PAN is the lightest PAN lookup: pass only the PAN number and get back the registered full name, the holder category (person / company / firm …) and the PAN status. Unlike PAN Lite and PAN Advanced — which require the PAN number, holder name and date of birth to compute match flags — this endpoint needs no name or DOB, so use it when you have to *discover* the holder's name rather than verify one you already hold.",
7099
+ "bestFor": "Resolving a PAN number to its registered name when no name or DOB is available",
7100
+ "docsUrl": "https://eps.eko.in/docs/pan-fetch",
7101
+ "headers": [
7102
+ {
7103
+ "name": "developer_key",
7104
+ "in": "header",
7105
+ "type": "string",
7106
+ "required": true,
7107
+ "description": "Static API key issued to your account after KYC."
7108
+ },
7109
+ {
7110
+ "name": "secret-key",
7111
+ "in": "header",
7112
+ "type": "string",
7113
+ "required": true,
7114
+ "description": "Dynamic per-request signature: base64(HMAC-SHA256(timestamp, base64(access_key)))."
7115
+ },
7116
+ {
7117
+ "name": "secret-key-timestamp",
7118
+ "in": "header",
7119
+ "type": "string",
7120
+ "required": true,
7121
+ "description": "Current time in milliseconds since UNIX epoch, used to compute secret-key. Must match server time."
7122
+ },
7123
+ {
7124
+ "name": "content-type",
7125
+ "in": "header",
7126
+ "type": "string",
7127
+ "required": true,
7128
+ "description": "application/json",
7129
+ "example": "application/json"
7130
+ }
7131
+ ],
7132
+ "requestParams": [
7133
+ {
7134
+ "name": "initiator_id",
7135
+ "type": "string",
7136
+ "required": true,
7137
+ "description": "Registered mobile number of the API user (see Platform Credentials).",
7138
+ "example": "9962981729",
7139
+ "in": "body"
7140
+ },
7141
+ {
7142
+ "name": "client_ref_id",
7143
+ "type": "string",
7144
+ "required": false,
7145
+ "description": "Unique reference ID per API call, generated by your system (max 20 characters).",
7146
+ "example": "2026010100123456789",
7147
+ "in": "body"
7148
+ },
7149
+ {
7150
+ "name": "pan_number",
7151
+ "label": "PAN Number",
7152
+ "type": "string",
7153
+ "required": true,
7154
+ "description": "10-character alphanumeric PAN identifier (5 letters, 4 digits, 1 letter).",
7155
+ "example": "GGTPB7880Q",
7156
+ "in": "body"
7157
+ },
7158
+ {
7159
+ "name": "source",
7160
+ "type": "string",
7161
+ "required": false,
7162
+ "description": "Origin of the request. Use 'API' for server-to-server calls.",
7163
+ "example": "API",
7164
+ "in": "body"
7165
+ }
7166
+ ],
7167
+ "sampleRequest": {
7168
+ "initiator_id": "9962981729",
7169
+ "client_ref_id": "2026010100123456789",
7170
+ "pan_number": "GGTPB7880Q",
7171
+ "source": "API"
7172
+ },
7173
+ "responseFields": [
7174
+ {
7175
+ "name": "status",
7176
+ "type": "number",
7177
+ "description": "Primary success indicator (0 = success).",
7178
+ "example": 0
7179
+ },
7180
+ {
7181
+ "name": "message",
7182
+ "type": "string",
7183
+ "description": "Human-readable response / error message.",
7184
+ "example": "Verification successful"
7185
+ },
7186
+ {
7187
+ "name": "response_status_id",
7188
+ "type": "number",
7189
+ "description": "Granular status id; see the shared error-codes table.",
7190
+ "example": 0
7191
+ },
7192
+ {
7193
+ "name": "response_type_id",
7194
+ "type": "number",
7195
+ "description": "A unique id for every possible response shape (success or error) — useful for client logic branching and analytics.",
7196
+ "example": 1388
7197
+ },
7198
+ {
7199
+ "name": "data",
7200
+ "type": "object",
7201
+ "description": "API-specific response payload.",
7202
+ "children": [
7203
+ {
7204
+ "name": "upstream_rrn",
7205
+ "label": "Upstream Reference",
7206
+ "type": "string",
7207
+ "description": "Reference number of the lookup at the upstream source.",
7208
+ "example": "pan_ZLpBnmwapOwtixPUmijB"
7209
+ },
7210
+ {
7211
+ "name": "pan_no",
7212
+ "label": "PAN Number",
7213
+ "type": "string",
7214
+ "description": "The PAN number submitted in the request.",
7215
+ "imp": true,
7216
+ "example": "GGTPB7880Q"
7217
+ },
7218
+ {
7219
+ "name": "fullname",
7220
+ "label": "Full Name",
7221
+ "type": "string",
7222
+ "description": "Full name registered against the PAN.",
7223
+ "imp": true,
7224
+ "example": "YASHWANT BASNETT"
7225
+ },
7226
+ {
7227
+ "name": "category",
7228
+ "type": "string",
7229
+ "description": "PAN holder category, e.g. 'person', 'company', 'firm', 'trust', 'huf'.",
7230
+ "imp": true,
7231
+ "example": "person"
7232
+ },
7233
+ {
7234
+ "name": "status",
7235
+ "label": "Lookup Status",
7236
+ "type": "string",
7237
+ "description": "Outcome of the PAN lookup, e.g. 'success'.",
7238
+ "imp": true,
7239
+ "example": "success"
7240
+ }
7241
+ ]
7242
+ }
7243
+ ],
7244
+ "sampleSuccessResponse": {
7245
+ "response_status_id": 0,
7246
+ "data": {
7247
+ "upstream_rrn": "pan_ZLpBnmwapOwtixPUmijB",
7248
+ "pan_no": "GGTPB7880Q",
7249
+ "fullname": "YASHWANT BASNETT",
7250
+ "category": "person",
7251
+ "status": "success"
7252
+ },
7253
+ "response_type_id": 0,
7254
+ "status": 0
7255
+ },
7256
+ "errorScenarios": [],
7257
+ "responseTypes": []
7258
+ },
7083
7259
  {
7084
7260
  "slug": "pan-lite",
7085
7261
  "productId": "pan",
package/dist/index.js CHANGED
@@ -67,6 +67,152 @@ var getApi = (bundle, slug) => bundle.apis.find((a) => a.slug === slug);
67
67
  var getTopic = (bundle, topic) => bundle.topics[topic];
68
68
  var getRecipe = (bundle, id) => bundle.recipes.find((r) => r.id === id);
69
69
 
70
+ // src/auth-debug.ts
71
+ var MS_DIGITS = 13;
72
+ var SECONDS_DIGITS = 10;
73
+ var DRIFT_WARN_MS = 5 * 60 * 1e3;
74
+ var supplied = (value) => {
75
+ const trimmed = value?.trim();
76
+ return trimmed ? trimmed : void 0;
77
+ };
78
+ var checkTimestamp = (value, nowMs) => {
79
+ const timestamp = supplied(value);
80
+ if (!timestamp)
81
+ return [
82
+ {
83
+ name: "timestamp",
84
+ ok: null,
85
+ detail: "No timestamp supplied. Pass the exact secret-key-timestamp you sent."
86
+ }
87
+ ];
88
+ if (!/^\d+$/.test(timestamp))
89
+ return [
90
+ {
91
+ name: "timestamp_format",
92
+ ok: false,
93
+ detail: "Not a plain digit string. The timestamp is signed verbatim, so quotes, a decimal point, a '+' or whitespace all change the signature."
94
+ }
95
+ ];
96
+ if (timestamp.length === SECONDS_DIGITS)
97
+ return [
98
+ {
99
+ name: "timestamp_unit",
100
+ ok: false,
101
+ detail: "10 digits \u2014 these are epoch SECONDS. Eko expects MILLISECONDS (13 digits): use Date.now(), time.time()*1000, or System.currentTimeMillis()."
102
+ }
103
+ ];
104
+ if (timestamp.length !== MS_DIGITS)
105
+ return [
106
+ {
107
+ name: "timestamp_unit",
108
+ ok: false,
109
+ detail: `${timestamp.length} digits \u2014 epoch milliseconds is ${MS_DIGITS}. Check for a truncated or padded value.`
110
+ }
111
+ ];
112
+ const driftMs = Number(timestamp) - nowMs;
113
+ const magnitude = Math.abs(driftMs);
114
+ const direction = driftMs > 0 ? "in the future" : "in the past";
115
+ return [
116
+ { name: "timestamp_unit", ok: true, detail: "13 digits \u2014 milliseconds." },
117
+ {
118
+ name: "clock_drift",
119
+ ok: magnitude <= DRIFT_WARN_MS,
120
+ detail: magnitude <= DRIFT_WARN_MS ? `${driftMs} ms from this server's clock \u2014 within the ${DRIFT_WARN_MS} ms heuristic.` : `${magnitude} ms ${direction} vs this server's clock (heuristic threshold ${DRIFT_WARN_MS} ms). Either the machine's clock is wrong (check NTP) or the timestamp is being reused instead of regenerated per request.`
121
+ }
122
+ ];
123
+ };
124
+ var checkSignatureShape = (value) => {
125
+ const signature = supplied(value);
126
+ if (!signature)
127
+ return [
128
+ {
129
+ name: "signature_shape",
130
+ ok: null,
131
+ detail: "No secret-key supplied. Pass the signature your code produced \u2014 never the access_key it was derived from."
132
+ }
133
+ ];
134
+ if (value !== signature)
135
+ return [
136
+ {
137
+ name: "signature_shape",
138
+ ok: false,
139
+ detail: "Leading/trailing whitespace or a trailing newline. Header values are sent verbatim \u2014 strip it (a common artefact of reading the key from a file)."
140
+ }
141
+ ];
142
+ if (/^[0-9a-f]{64}$/i.test(signature))
143
+ return [
144
+ {
145
+ name: "signature_shape",
146
+ ok: false,
147
+ detail: "64 hex characters \u2014 this is the raw HMAC digest. The final base64 step was skipped: base64-encode the digest bytes."
148
+ }
149
+ ];
150
+ if (/[-_]/.test(signature))
151
+ return [
152
+ {
153
+ name: "signature_shape",
154
+ ok: false,
155
+ detail: "Contains '-' or '_' \u2014 URL-safe base64. Eko expects the standard alphabet ('+' and '/'), padded with '='."
156
+ }
157
+ ];
158
+ const decoded = Buffer.from(signature, "base64");
159
+ if (decoded.toString("base64") !== signature)
160
+ return [
161
+ {
162
+ name: "signature_shape",
163
+ ok: false,
164
+ detail: "Not canonical base64 \u2014 wrong padding or an out-of-alphabet character."
165
+ }
166
+ ];
167
+ if (decoded.length !== 32)
168
+ return [
169
+ {
170
+ name: "signature_shape",
171
+ ok: false,
172
+ detail: `Decodes to ${decoded.length} bytes; SHA-256 is 32. Check the hash algorithm \u2014 SHA-1 gives 20, SHA-512 gives 64.`
173
+ }
174
+ ];
175
+ return [
176
+ {
177
+ name: "signature_shape",
178
+ ok: true,
179
+ detail: "Canonical base64 of 32 bytes \u2014 the right shape for HMAC-SHA256. Shape alone cannot prove the value: run the test vector."
180
+ }
181
+ ];
182
+ };
183
+ var RANKED_403_CAUSES = [
184
+ {
185
+ id: "ip_not_allowlisted",
186
+ cause: "The calling server's public IP is not allowlisted for that key.",
187
+ fix: "Send your egress IP to Eko support. Note that serverless/dynamic egress (Vercel, Lambda) cannot be allowlisted \u2014 call from a fixed-IP host."
188
+ },
189
+ {
190
+ id: "key_inactive",
191
+ cause: "The key is not active, or not provisioned for that environment.",
192
+ fix: "Confirm with Eko that the keypair is live for the environment you are calling."
193
+ },
194
+ {
195
+ id: "environment_mismatch",
196
+ cause: "UAT credentials sent to the production base URL, or the reverse \u2014 the keys are environment-specific.",
197
+ fix: "Check the base URL against the environments topic; the developer_key and access_key must come from the same environment."
198
+ },
199
+ {
200
+ id: "header_name_typo",
201
+ cause: "Header spelled wrongly: `secret_key`/`secretKey` instead of `secret-key`, or `developer-key` instead of `developer_key`.",
202
+ fix: "Header names are exactly: developer_key, secret-key, secret-key-timestamp, content-type."
203
+ },
204
+ {
205
+ id: "timestamp_mismatch",
206
+ cause: "The signed timestamp is not the one sent in `secret-key-timestamp` \u2014 often a second Date.now() call, or a value cached across requests.",
207
+ fix: "Compute the timestamp once, sign that exact string, and send the same string."
208
+ },
209
+ {
210
+ id: "key_decoded_before_signing",
211
+ cause: "The base64 of the access_key was decoded back to bytes before being used as the HMAC key.",
212
+ fix: "The HMAC key is the base64 STRING itself, used as-is. Confirm with the test vector."
213
+ }
214
+ ];
215
+
70
216
  // src/signing-snippets.ts
71
217
  var SIGNING_LANGUAGES = [
72
218
  "php",
@@ -290,6 +436,28 @@ var createEpsServer = (bundle, source, versionState) => {
290
436
  content: [{ type: "text", text: getSigningSnippet(language) }]
291
437
  })
292
438
  );
439
+ server.registerTool(
440
+ "debug_auth",
441
+ {
442
+ title: "Debug auth / 403",
443
+ description: "Diagnose a 403 from an EPS API. Returns a known-answer TEST VECTOR: run your own signing code over test_vector.accessKey + test_vector.timestamp \u2014 if you reproduce test_vector.secretKey, your HMAC is correct, so stop debugging the algorithm and work through ranked_causes instead. Optionally pass the timestamp and secret-key from the failing request and they are checked for the mechanical faults (seconds instead of milliseconds, clock drift, wrong digest length, stray newline). SECRET-FREE BY DESIGN: there is no access_key parameter and there never will be \u2014 never paste an access_key into a tool call; it is a server-side secret.",
444
+ inputSchema: {
445
+ timestamp: z.string().optional().describe("The secret-key-timestamp sent on the failing request."),
446
+ secret_key: z.string().optional().describe("The secret-key your code produced. Never the access_key.")
447
+ },
448
+ annotations: READ_ONLY
449
+ },
450
+ async ({ timestamp, secret_key }) => json({
451
+ test_vector: bundle.topics.auth.testVector,
452
+ how_to_use_test_vector: "secret-key = base64(HMAC_SHA256(key = base64(access_key) AS A STRING, message = timestamp)). Reproduce test_vector.secretKey from the vector's inputs to prove your implementation.",
453
+ checks: [
454
+ ...checkTimestamp(timestamp, Date.now()),
455
+ ...checkSignatureShape(secret_key)
456
+ ],
457
+ ranked_causes: RANKED_403_CAUSES,
458
+ docs_url: bundle.topics.auth.docsUrl
459
+ })
460
+ );
293
461
  server.registerTool(
294
462
  "get_meta",
295
463
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekoindia/eps-context-mcp",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
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",