@ekoindia/eps-transact-mcp 0.1.0 → 0.1.1

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
@@ -55,13 +55,15 @@ Optional env: `EKO_ENV` (`uat` default | `production`), `EKO_ALLOWED_APIS` (defa
55
55
 
56
56
  ## Staying up to date
57
57
 
58
- - **Remote server** — nothing to do. It's hosted; the operator redeploys and every client is instantly current. `GET /healthz` reports the live `bundleVersion`.
58
+ - **Remote server** — nothing to do. It's hosted and auto-redeploys on every published update (its poller pulls the new `:prod`), so every client is instantly current. `GET /healthz` reports the live `bundleVersion`.
59
59
  - **Local stdio** — the `@latest` in the install command re-resolves the newest published version on every launch, so `npx` always fetches current. `@latest` does a registry lookup at start; the server runs fully offline after that. Offline/air-gapped? Pin a version: `npx --offline -y @ekoindia/eps-transact-mcp@<version>`.
60
60
  - **Update check** — on startup the stdio bin does one best-effort `GET registry.npmjs.org/@ekoindia/eps-transact-mcp/latest` (3s timeout, silent on any failure) and, if your config pinned an older version, prints a one-line stderr nudge. It never blocks startup, sends no data, and touches nothing but stderr. Disable with `EPS_NO_UPDATE_CHECK=1` (corporate/no-egress). The remote server does **not** run this check.
61
61
 
62
62
  ## Tools
63
63
 
64
- One tool per verification endpoint, named `eps_<slug with underscores>` — e.g. `eps_pan_lite`, `eps_bank_account_verification`, `eps_verify_gstin`, `eps_driving_license`. Every tool description carries the billing reminder; input schemas (required params, types, examples) are generated from the API specs. Multi-step flows (mobile OTP, DigiLocker) are exposed as their individual steps — the server is stateless; your agent carries intermediate ids between calls.
64
+ One tool per verification endpoint, named `eps_<slug with underscores>` — e.g. `eps_pan_lite`, `eps_bank_account_verification`, `eps_verify_gstin`, `eps_driving_license`. Every tool description carries the billing reminder; input schemas (required params, types, examples — including item shapes for array params like bulk `entries`) are generated from the API specs. Multi-step flows (mobile OTP, DigiLocker, bulk verify) are exposed as their individual steps — the server is stateless; your agent carries intermediate ids between calls, and each step's description says which tool comes next and which field carries over.
65
+
66
+ Every tool declares MCP annotations: `openWorldHint: true` on all (they call a paid external API), and `readOnlyHint: false` + `idempotentHint: false` on the side-effecting ones — `eps_bank_account_verification` (live, non-refundable ₹1 penny-drop), the two bulk-submit tools (enqueue async batches), `eps_mobile_otp_send`/`eps_mobile_otp_verify` (send/consume an OTP), and `eps_digilocker_create_url` (creates a consent session). Their descriptions state the side effect instead of "Read-only verification". Hosts should gate side-effecting tools accordingly; billing applies to **all** successful calls either way.
65
67
 
66
68
  Errors come back sanitized as `{ code, message }`: `VALIDATION` (names the missing/invalid params — never their values), `TOOL_NOT_ALLOWED`, `UNKNOWN_TOOL`, `UPSTREAM_TIMEOUT`, `UPSTREAM_ERROR`. Successful upstream envelopes (including business failures like "PAN not found") are returned verbatim — they are your data.
67
69
 
@@ -84,7 +86,9 @@ npm run transact:dev # HTTP server on :8788 with tsx watch
84
86
  npm run transact:typecheck
85
87
  ```
86
88
 
87
- Tests never call Eko; upstream fetch is injected. The one exception is the env-gated live UAT smoke:
89
+ Four utils (`fetchTimeout`, `requestId`, `accessLog`, `update-check`) are hand-copied/adapted from eps-backend / eps-context-mcp; `parity.copied-utils.test.ts` pins a content hash of both sides of each pair, so editing either file fails the suite until you diff the pair, port what applies, and paste the replacement hash printed in the failure.
90
+
91
+ Tests never call Eko; upstream fetch is injected. The one exception is the opt-in live UAT smoke (`uat-smoke.test.ts`), which self-skips unless real UAT credentials are set. Copy `.env.example` to `.env` in this package (gitignored; vitest loads `EPS_UAT_*` from it) or pass the vars inline — inline wins:
88
92
 
89
93
  ```sh
90
94
  EPS_UAT_DEVELOPER_KEY=… EPS_UAT_ACCESS_KEY=… EPS_UAT_INITIATOR_ID=… \
package/data/eps.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "meta": {
3
3
  "org": "ekoindia",
4
4
  "apiVersion": "v3",
5
- "bundleVersion": "5f1b3ac4",
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>",
@@ -26,12 +26,62 @@ var EXECUTOR_HEADER_NAMES = /* @__PURE__ */ new Set([
26
26
  "content-type"
27
27
  ]);
28
28
  var IDENTITY_PARAMS = /* @__PURE__ */ new Set(["initiator_id", "user_code"]);
29
+ var SIDE_EFFECTS = {
30
+ "bank-account-verification": "Performs a live, non-refundable \u20B91 penny-drop to the target account",
31
+ "bulk-bank-account-verification": "Enqueues an async penny-drop verification batch",
32
+ "pan-bulk-verify": "Enqueues an async PAN verification batch",
33
+ "mobile-otp-send": "Sends an OTP SMS to the target mobile number",
34
+ "mobile-otp-verify": "Consumes the OTP and advances the verification session",
35
+ "digilocker-create-url": "Creates a DigiLocker consent session"
36
+ };
37
+ var FLOW_GUIDANCE = {
38
+ "pan-bulk-verify": "Then poll eps_pan_bulk_status with the returned reference_id.",
39
+ "pan-bulk-status": "Poll step: pass the reference_id returned by eps_pan_bulk_verify.",
40
+ "bulk-bank-account-verification": "Then poll eps_bulk_bank_account_verification_status with the returned bulk_reference_id.",
41
+ "bulk-bank-account-verification-status": "Poll step: pass the bulk_reference_id returned by eps_bulk_bank_account_verification.",
42
+ "digilocker-create-url": "Flow step 1/3: after user consent, call eps_digilocker_get_document with the returned verification_id and reference_id.",
43
+ "digilocker-get-document": "Flow step 2/3: pass the verification_id and reference_id from eps_digilocker_create_url.",
44
+ "digilocker-verification-status": "Flow step 3/3: pass the reference_id from eps_digilocker_create_url.",
45
+ "mobile-otp-send": "Flow step 1/3: then call eps_mobile_otp_verify with the OTP the user received.",
46
+ "mobile-otp-verify": "Flow step 2/3: use the same mobile as eps_mobile_otp_send; returns an otp_verification_token.",
47
+ "mobile-otp-validate-token": "Flow step 3/3: pass the otp_verification_token returned by eps_mobile_otp_verify."
48
+ };
29
49
  var toToolName = (slug) => `eps_${slug.replace(/-/g, "_")}`;
30
50
  var verificationApis = (bundle) => bundle.apis.filter((a) => a.category === "verification" && !a.financial);
31
51
  var JSON_SCHEMA_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean"]);
32
- var paramToJsonSchema = (p) => {
52
+ var scalarTypeOf = (value) => {
53
+ if (typeof value === "string") return "string";
54
+ if (typeof value === "number") return "number";
55
+ if (typeof value === "boolean") return "boolean";
56
+ return void 0;
57
+ };
58
+ var arrayItems = (p, children) => {
59
+ if (children.length) {
60
+ const properties = {};
61
+ for (const c of children)
62
+ properties[c.name.slice(p.name.length + 3)] = paramToJsonSchema(c);
63
+ const required = children.filter((c) => c.required).map((c) => c.name.slice(p.name.length + 3));
64
+ return { type: "object", properties, required };
65
+ }
66
+ const first = Array.isArray(p.example) ? p.example[0] : void 0;
67
+ if (first !== null && typeof first === "object") {
68
+ const properties = {};
69
+ for (const [key, value] of Object.entries(first)) {
70
+ const type2 = scalarTypeOf(value);
71
+ properties[key] = type2 ? { type: type2 } : {};
72
+ }
73
+ return { type: "object", properties };
74
+ }
75
+ const type = scalarTypeOf(first);
76
+ return type ? { type } : {};
77
+ };
78
+ var paramToJsonSchema = (p, arrayChildren = []) => {
33
79
  const schema = {};
34
80
  if (JSON_SCHEMA_TYPES.has(p.type)) schema.type = p.type;
81
+ else if (p.type === "array") {
82
+ schema.type = "array";
83
+ schema.items = arrayItems(p, arrayChildren);
84
+ }
35
85
  const description = [
36
86
  p.description ?? p.label ?? p.name,
37
87
  p.example !== void 0 ? `Example: ${JSON.stringify(p.example)}` : ""
@@ -47,15 +97,33 @@ var buildToolDefs = (bundle) => verificationApis(bundle).map((api) => {
47
97
  throw new Error(
48
98
  `Spec "${api.slug}" requires header(s) ${unrepresentable.map((h) => h.name).join(", ")} which the transactional executor cannot send. Add header support to the executor before exposing this tool.`
49
99
  );
100
+ const topLevel = api.requestParams.filter((p) => !p.name.includes("[]."));
101
+ const childrenOf = (parent) => api.requestParams.filter((p) => p.name.startsWith(`${parent.name}[].`));
50
102
  const properties = {};
51
- for (const p of api.requestParams)
52
- properties[p.name] = paramToJsonSchema(p);
53
- const required = api.requestParams.filter((p) => p.required).map((p) => p.name);
103
+ for (const p of topLevel)
104
+ properties[p.name] = paramToJsonSchema(p, childrenOf(p));
105
+ const required = topLevel.filter((p) => p.required).map((p) => p.name);
106
+ const sideEffect = SIDE_EFFECTS[api.slug];
107
+ const description = [
108
+ api.summary,
109
+ sideEffect ? `${sideEffect} via Eko EPS (${api.method} ${api.path}).` : `Read-only verification via Eko EPS (${api.method} ${api.path}).`,
110
+ "Each successful call is billed per EPS pricing.",
111
+ FLOW_GUIDANCE[api.slug]
112
+ ].filter(Boolean).join(" ");
54
113
  return {
55
114
  name: toToolName(api.slug),
56
115
  slug: api.slug,
57
- description: `${api.summary} Read-only verification via Eko EPS (${api.method} ${api.path}). Each successful call is billed per EPS pricing.`,
58
- inputSchema: { type: "object", properties, required }
116
+ title: api.name,
117
+ description,
118
+ inputSchema: { type: "object", properties, required },
119
+ annotations: {
120
+ readOnlyHint: !sideEffect,
121
+ // Meaningful only when readOnlyHint is false (MCP spec); every
122
+ // side-effecting call here does something new (new penny-drop,
123
+ // new SMS, new batch, new session).
124
+ ...sideEffect && { idempotentHint: false },
125
+ openWorldHint: true
126
+ }
59
127
  };
60
128
  });
61
129
 
@@ -112,7 +180,9 @@ var createTransactServer = (tools, ctx, version) => {
112
180
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
113
181
  tools: visibleTools.map((t) => ({
114
182
  name: t.name,
183
+ title: t.title,
115
184
  description: t.description,
185
+ annotations: t.annotations,
116
186
  // Identity params covered by a server-side default are demoted from
117
187
  // `required` so schema-validating hosts don't force the model to
118
188
  // invent them; EpsClient still enforces presence after merging.
@@ -151,9 +221,7 @@ var createTransactServer = (tools, ctx, version) => {
151
221
  req.params.arguments ?? {}
152
222
  );
153
223
  return {
154
- content: [
155
- { type: "text", text: JSON.stringify(result, null, 2) }
156
- ]
224
+ content: [{ type: "text", text: JSON.stringify(result) }]
157
225
  };
158
226
  } catch (err) {
159
227
  return errorResult(sanitizeError(err));
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  parseAllowed,
7
7
  parseEnvironment,
8
8
  withTimeout
9
- } from "./chunk-AAUR66A3.js";
9
+ } from "./chunk-XLZLL6DZ.js";
10
10
 
11
11
  // src/index.ts
12
12
  import { serve } from "@hono/node-server";
package/dist/stdio.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  parseAllowed,
7
7
  parseEnvironment,
8
8
  withTimeout
9
- } from "./chunk-AAUR66A3.js";
9
+ } from "./chunk-XLZLL6DZ.js";
10
10
 
11
11
  // src/stdio.ts
12
12
  import { createRequire } from "module";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekoindia/eps-transact-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Transactional MCP server for Eko Platform Services (EPS) verification APIs — remote (streamable HTTP) and local (stdio).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -29,7 +29,7 @@
29
29
  "prepublishOnly": "npm run bake && npm run build"
30
30
  },
31
31
  "dependencies": {
32
- "@ekoindia/eps-sdk": "*",
32
+ "@ekoindia/eps-sdk": "^0.1.1",
33
33
  "@hono/node-server": "^1.0.0",
34
34
  "@modelcontextprotocol/sdk": "^1.29.0",
35
35
  "hono": "^4.0.0"