@ekoindia/eps-transact-mcp 0.1.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.
@@ -0,0 +1,181 @@
1
+ // src/load-bundle.ts
2
+ import { promises as fs } from "fs";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+ var here = path.dirname(fileURLToPath(import.meta.url));
6
+ var BAKED_PATH = path.resolve(here, "../data/eps.json");
7
+ var loadBundle = async () => {
8
+ const url = process.env.EPS_BUNDLE_URL;
9
+ if (url) {
10
+ try {
11
+ const res = await fetch(url);
12
+ if (res.ok)
13
+ return { bundle: await res.json(), source: "remote" };
14
+ } catch {
15
+ }
16
+ }
17
+ const raw = await fs.readFile(BAKED_PATH, "utf8");
18
+ return { bundle: JSON.parse(raw), source: "baked" };
19
+ };
20
+
21
+ // src/tools.ts
22
+ var EXECUTOR_HEADER_NAMES = /* @__PURE__ */ new Set([
23
+ "developer_key",
24
+ "secret-key",
25
+ "secret-key-timestamp",
26
+ "content-type"
27
+ ]);
28
+ var IDENTITY_PARAMS = /* @__PURE__ */ new Set(["initiator_id", "user_code"]);
29
+ var toToolName = (slug) => `eps_${slug.replace(/-/g, "_")}`;
30
+ var verificationApis = (bundle) => bundle.apis.filter((a) => a.category === "verification" && !a.financial);
31
+ var JSON_SCHEMA_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean"]);
32
+ var paramToJsonSchema = (p) => {
33
+ const schema = {};
34
+ if (JSON_SCHEMA_TYPES.has(p.type)) schema.type = p.type;
35
+ const description = [
36
+ p.description ?? p.label ?? p.name,
37
+ p.example !== void 0 ? `Example: ${JSON.stringify(p.example)}` : ""
38
+ ].filter(Boolean).join(" ");
39
+ if (description) schema.description = description;
40
+ return schema;
41
+ };
42
+ var buildToolDefs = (bundle) => verificationApis(bundle).map((api) => {
43
+ const unrepresentable = api.headers.filter(
44
+ (h) => h.required && !EXECUTOR_HEADER_NAMES.has(h.name)
45
+ );
46
+ if (unrepresentable.length)
47
+ throw new Error(
48
+ `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
+ );
50
+ 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);
54
+ return {
55
+ name: toToolName(api.slug),
56
+ 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 }
59
+ };
60
+ });
61
+
62
+ // src/ctx.ts
63
+ var parseEnvironment = (raw) => {
64
+ const value = (raw ?? "uat").trim().toLowerCase();
65
+ if (value === "uat") return "sandbox";
66
+ if (value === "production") return "production";
67
+ return void 0;
68
+ };
69
+ var parseAllowed = (raw) => {
70
+ const value = raw?.trim();
71
+ if (!value) return void 0;
72
+ if (value === "*") return "all";
73
+ const names = value.split(",").map((s) => s.trim()).filter(Boolean);
74
+ return names.length ? new Set(names) : void 0;
75
+ };
76
+ var isAllowed = (ctx, toolName) => ctx.allowed === "all" || ctx.allowed.has(toolName);
77
+
78
+ // src/server.ts
79
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
80
+ import {
81
+ CallToolRequestSchema,
82
+ ListToolsRequestSchema
83
+ } from "@modelcontextprotocol/sdk/types.js";
84
+ import { EpsClient } from "@ekoindia/eps-sdk";
85
+ var SAFE_MESSAGE_PATTERNS = [
86
+ /^Missing required params for /,
87
+ /^Invalid param types for /,
88
+ /^Unknown endpoint slug /,
89
+ /^Unknown environment /
90
+ ];
91
+ var sanitizeError = (err) => {
92
+ const message = err instanceof Error ? err.message : String(err);
93
+ if (SAFE_MESSAGE_PATTERNS.some((re) => re.test(message)))
94
+ return { code: "VALIDATION", message };
95
+ if (err instanceof Error && err.name === "TimeoutError")
96
+ return { code: "UPSTREAM_TIMEOUT", message: "Eko EPS request timed out." };
97
+ return {
98
+ code: "UPSTREAM_ERROR",
99
+ message: "Eko EPS call failed (network or non-JSON upstream response)."
100
+ };
101
+ };
102
+ var errorResult = (payload) => ({
103
+ isError: true,
104
+ content: [{ type: "text", text: JSON.stringify(payload) }]
105
+ });
106
+ var createTransactServer = (tools, ctx, version) => {
107
+ const server = new Server(
108
+ { name: "eps-transact-mcp", version },
109
+ { capabilities: { tools: {} } }
110
+ );
111
+ const visibleTools = tools.filter((t) => isAllowed(ctx, t.name));
112
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
113
+ tools: visibleTools.map((t) => ({
114
+ name: t.name,
115
+ description: t.description,
116
+ // Identity params covered by a server-side default are demoted from
117
+ // `required` so schema-validating hosts don't force the model to
118
+ // invent them; EpsClient still enforces presence after merging.
119
+ inputSchema: {
120
+ ...t.inputSchema,
121
+ required: t.inputSchema.required.filter(
122
+ (name) => !(IDENTITY_PARAMS.has(name) && (name === "initiator_id" && ctx.initiatorId !== void 0 || name === "user_code" && ctx.userCode !== void 0))
123
+ )
124
+ }
125
+ }))
126
+ }));
127
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
128
+ const tool = tools.find((t) => t.name === req.params.name);
129
+ if (!tool)
130
+ return errorResult({
131
+ code: "UNKNOWN_TOOL",
132
+ message: `Unknown tool "${req.params.name}".`
133
+ });
134
+ if (!isAllowed(ctx, tool.name))
135
+ return errorResult({
136
+ code: "TOOL_NOT_ALLOWED",
137
+ message: `Tool "${tool.name}" is not in this connection's X-Eko-Allowed-Apis allowlist.`
138
+ });
139
+ const client = new EpsClient({
140
+ developerKey: ctx.developerKey,
141
+ accessKey: ctx.accessKey,
142
+ environment: ctx.environment,
143
+ ...ctx.initiatorId !== void 0 && { initiatorId: ctx.initiatorId },
144
+ ...ctx.userCode !== void 0 && { userCode: ctx.userCode },
145
+ ...ctx.fetch && { fetch: ctx.fetch },
146
+ ...ctx.now && { now: ctx.now }
147
+ });
148
+ try {
149
+ const result = await client.call(
150
+ tool.slug,
151
+ req.params.arguments ?? {}
152
+ );
153
+ return {
154
+ content: [
155
+ { type: "text", text: JSON.stringify(result, null, 2) }
156
+ ]
157
+ };
158
+ } catch (err) {
159
+ return errorResult(sanitizeError(err));
160
+ }
161
+ });
162
+ return server;
163
+ };
164
+
165
+ // src/fetchTimeout.ts
166
+ var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
167
+ function withTimeout(fetchImpl, ms = DEFAULT_FETCH_TIMEOUT_MS) {
168
+ return (input, init) => fetchImpl(input, {
169
+ ...init,
170
+ signal: init?.signal ?? AbortSignal.timeout(ms)
171
+ });
172
+ }
173
+
174
+ export {
175
+ loadBundle,
176
+ buildToolDefs,
177
+ parseEnvironment,
178
+ parseAllowed,
179
+ createTransactServer,
180
+ withTimeout
181
+ };
package/dist/index.js ADDED
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ buildToolDefs,
4
+ createTransactServer,
5
+ loadBundle,
6
+ parseAllowed,
7
+ parseEnvironment,
8
+ withTimeout
9
+ } from "./chunk-AAUR66A3.js";
10
+
11
+ // src/index.ts
12
+ import { serve } from "@hono/node-server";
13
+
14
+ // src/http.ts
15
+ import { createHash } from "crypto";
16
+ import { Hono } from "hono";
17
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
18
+
19
+ // src/requestId.ts
20
+ import { randomUUID } from "crypto";
21
+ var FALLBACK_RID = "rid-unavailable";
22
+ function sanitizeRequestId(raw) {
23
+ if (!raw) return "";
24
+ return raw.replace(/[^A-Za-z0-9._-]/g, "").slice(0, 128);
25
+ }
26
+ function requestId(opts = {}) {
27
+ const genId = opts.genId ?? (() => randomUUID());
28
+ return async (c, next) => {
29
+ let rid = sanitizeRequestId(c.req.header("x-request-id"));
30
+ if (!rid) {
31
+ try {
32
+ rid = genId();
33
+ } catch {
34
+ rid = FALLBACK_RID;
35
+ }
36
+ }
37
+ c.set("rid", rid);
38
+ c.header("x-request-id", rid);
39
+ await next();
40
+ };
41
+ }
42
+
43
+ // src/accessLog.ts
44
+ function createAccessLogger(opts = {}) {
45
+ const sink = opts.sink ?? ((line) => console.log(line));
46
+ const now = opts.now ?? (() => /* @__PURE__ */ new Date());
47
+ return {
48
+ log(input) {
49
+ try {
50
+ const record = {
51
+ type: "access",
52
+ ts: now().toISOString(),
53
+ ...input
54
+ };
55
+ void Promise.resolve(sink(JSON.stringify(record))).catch(() => {
56
+ });
57
+ } catch {
58
+ }
59
+ }
60
+ };
61
+ }
62
+ var noopAccessLogger = {
63
+ log() {
64
+ }
65
+ };
66
+
67
+ // src/http.ts
68
+ var RL_LIMIT = 60;
69
+ var RL_WINDOW_SEC = 600;
70
+ var rpcError = (code, message) => ({
71
+ jsonrpc: "2.0",
72
+ error: { code, message },
73
+ id: null
74
+ });
75
+ var extractToolName = async (raw) => {
76
+ try {
77
+ const body = await raw.json();
78
+ if (typeof body !== "object" || body === null || Array.isArray(body))
79
+ return void 0;
80
+ const rpc = body;
81
+ if (rpc.method !== "tools/call") return void 0;
82
+ return typeof rpc.params?.name === "string" ? rpc.params.name : void 0;
83
+ } catch {
84
+ return void 0;
85
+ }
86
+ };
87
+ var createApp = (deps) => {
88
+ const tools = buildToolDefs(deps.bundle);
89
+ const accessLog = deps.accessLog ?? noopAccessLogger;
90
+ const upstreamFetch = withTimeout(deps.fetch ?? fetch);
91
+ const rl = deps.rateLimit ?? { limit: RL_LIMIT, windowSec: RL_WINDOW_SEC };
92
+ const windows = /* @__PURE__ */ new Map();
93
+ const nowMs = deps.now ?? Date.now;
94
+ const overLimit = (key) => {
95
+ if (rl === false) return false;
96
+ const t = nowMs();
97
+ const w = windows.get(key);
98
+ if (!w || t >= w.resetAt) {
99
+ windows.set(key, { count: 1, resetAt: t + rl.windowSec * 1e3 });
100
+ return false;
101
+ }
102
+ w.count += 1;
103
+ return w.count > rl.limit;
104
+ };
105
+ const app = new Hono();
106
+ app.use(requestId());
107
+ app.use(async (c, next) => {
108
+ const start = nowMs();
109
+ try {
110
+ await next();
111
+ } finally {
112
+ if (c.req.path !== "/healthz")
113
+ accessLog.log({
114
+ rid: c.get("rid") ?? "rid-unavailable",
115
+ method: c.req.method,
116
+ path: c.req.path,
117
+ status: c.res?.status ?? 500,
118
+ durMs: nowMs() - start,
119
+ ip: c.req.header("x-real-ip") ?? "",
120
+ ...c.get("toolName") && { tool: c.get("toolName") }
121
+ });
122
+ }
123
+ });
124
+ app.get(
125
+ "/healthz",
126
+ (c) => c.json({
127
+ ok: true,
128
+ bundleVersion: deps.bundle.meta.bundleVersion,
129
+ tools: tools.length
130
+ })
131
+ );
132
+ app.post("/mcp", async (c) => {
133
+ const developerKey = c.req.header("x-eko-developer-key");
134
+ const accessKey = c.req.header("x-eko-access-key");
135
+ if (!developerKey || !accessKey)
136
+ return c.json(
137
+ rpcError(
138
+ -32001,
139
+ "Missing X-Eko-Developer-Key and/or X-Eko-Access-Key header."
140
+ ),
141
+ 401
142
+ );
143
+ const allowed = parseAllowed(c.req.header("x-eko-allowed-apis"));
144
+ if (!allowed)
145
+ return c.json(
146
+ rpcError(
147
+ -32602,
148
+ 'Missing or empty X-Eko-Allowed-Apis header. Send a comma-separated list of tool names, or "*" to allow all verification tools. This is deliberate: EPS calls are billed.'
149
+ ),
150
+ 400
151
+ );
152
+ const environment = parseEnvironment(c.req.header("x-eko-env"));
153
+ if (!environment)
154
+ return c.json(
155
+ rpcError(
156
+ -32602,
157
+ 'Invalid X-Eko-Env header. Use "uat" (default) or "production".'
158
+ ),
159
+ 400
160
+ );
161
+ const rlKey = `${environment}:${createHash("sha256").update(developerKey).digest("hex")}`;
162
+ if (overLimit(rlKey))
163
+ return c.json(
164
+ rpcError(-32e3, "Rate limit exceeded for this developer key."),
165
+ 429
166
+ );
167
+ c.set("toolName", await extractToolName(c.req.raw.clone()));
168
+ const ctx = {
169
+ developerKey,
170
+ accessKey,
171
+ environment,
172
+ allowed,
173
+ initiatorId: c.req.header("x-eko-initiator-id"),
174
+ userCode: c.req.header("x-eko-user-code"),
175
+ fetch: upstreamFetch,
176
+ ...deps.now && { now: deps.now }
177
+ };
178
+ const transport = new WebStandardStreamableHTTPServerTransport({
179
+ sessionIdGenerator: void 0,
180
+ // stateless: no session to manage
181
+ enableJsonResponse: true
182
+ });
183
+ const server = createTransactServer(
184
+ tools,
185
+ ctx,
186
+ deps.bundle.meta.bundleVersion
187
+ );
188
+ await server.connect(transport);
189
+ return transport.handleRequest(c.req.raw);
190
+ });
191
+ app.on(["GET", "DELETE"], "/mcp", (c) => {
192
+ c.header("Allow", "POST");
193
+ return c.json(rpcError(-32e3, "Method not allowed. POST only."), 405);
194
+ });
195
+ return app;
196
+ };
197
+
198
+ // src/index.ts
199
+ var main = async () => {
200
+ const { bundle, source } = await loadBundle();
201
+ const app = createApp({ bundle, accessLog: createAccessLogger() });
202
+ const port = Number(process.env.PORT ?? 8788);
203
+ const server = serve({ fetch: app.fetch, port, hostname: "0.0.0.0" }, () => {
204
+ console.error(
205
+ `[eps-transact-mcp] listening on :${port} (bundle ${bundle.meta.bundleVersion}, ${source})`
206
+ );
207
+ });
208
+ const shutdown = () => {
209
+ server.close(() => process.exit(0));
210
+ setTimeout(() => process.exit(0), 5e3).unref();
211
+ };
212
+ process.on("SIGTERM", shutdown);
213
+ process.on("SIGINT", shutdown);
214
+ };
215
+ main().catch((err) => {
216
+ console.error(
217
+ `[eps-transact-mcp] fatal: ${err instanceof Error ? err.message : String(err)}`
218
+ );
219
+ process.exit(1);
220
+ });
package/dist/stdio.js ADDED
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ buildToolDefs,
4
+ createTransactServer,
5
+ loadBundle,
6
+ parseAllowed,
7
+ parseEnvironment,
8
+ withTimeout
9
+ } from "./chunk-AAUR66A3.js";
10
+
11
+ // src/stdio.ts
12
+ import { createRequire } from "module";
13
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
+
15
+ // src/update-check.ts
16
+ var REGISTRY_URL = "https://registry.npmjs.org/@ekoindia/eps-transact-mcp/latest";
17
+ var parseStrict = (v) => {
18
+ const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(v.trim());
19
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
20
+ };
21
+ var isNewer = (current, latest) => {
22
+ const a = parseStrict(current);
23
+ const b = parseStrict(latest);
24
+ if (!a || !b) return void 0;
25
+ for (let i = 0; i < 3; i++) {
26
+ if (b[i] > a[i]) return true;
27
+ if (b[i] < a[i]) return false;
28
+ }
29
+ return false;
30
+ };
31
+ var checkForUpdate = async (current, deps = {}) => {
32
+ const fetchFn = deps.fetch ?? fetch;
33
+ try {
34
+ const res = await fetchFn(REGISTRY_URL, {
35
+ signal: AbortSignal.timeout(deps.timeoutMs ?? 3e3)
36
+ });
37
+ if (!res.ok) return { current };
38
+ const body = await res.json();
39
+ const latest = typeof body.version === "string" ? body.version : void 0;
40
+ if (!latest) return { current };
41
+ const newer = isNewer(current, latest);
42
+ return {
43
+ current,
44
+ latest,
45
+ ...newer !== void 0 && { updateAvailable: newer }
46
+ };
47
+ } catch {
48
+ return { current };
49
+ }
50
+ };
51
+ var updateNotice = (state) => `eps-transact-mcp ${state.latest} available (running ${state.current}). Use "npx -y @ekoindia/eps-transact-mcp@latest" in your MCP config to always run the newest version.`;
52
+ var notifyIfOutdated = async (current, deps = {}) => {
53
+ const env = deps.env ?? process.env;
54
+ if (env.EPS_NO_UPDATE_CHECK === "1") return;
55
+ const state = await checkForUpdate(current, { fetch: deps.fetch });
56
+ if (state.updateAvailable) (deps.warn ?? console.error)(updateNotice(state));
57
+ };
58
+
59
+ // src/stdio.ts
60
+ var { version: OWN_VERSION } = createRequire(import.meta.url)(
61
+ "../package.json"
62
+ );
63
+ var main = async () => {
64
+ const developerKey = process.env.EKO_DEVELOPER_KEY;
65
+ const accessKey = process.env.EKO_ACCESS_KEY;
66
+ if (!developerKey || !accessKey) {
67
+ console.error(
68
+ "[eps-transact-mcp] EKO_DEVELOPER_KEY and EKO_ACCESS_KEY env vars are required."
69
+ );
70
+ process.exit(1);
71
+ }
72
+ const environment = parseEnvironment(process.env.EKO_ENV);
73
+ if (!environment) {
74
+ console.error(
75
+ '[eps-transact-mcp] Invalid EKO_ENV. Use "uat" (default) or "production".'
76
+ );
77
+ process.exit(1);
78
+ }
79
+ const allowed = parseAllowed(process.env.EKO_ALLOWED_APIS) ?? "all";
80
+ const { bundle } = await loadBundle();
81
+ const ctx = {
82
+ developerKey,
83
+ accessKey,
84
+ environment,
85
+ allowed,
86
+ initiatorId: process.env.EKO_INITIATOR_ID,
87
+ userCode: process.env.EKO_USER_CODE,
88
+ fetch: withTimeout(fetch)
89
+ };
90
+ const server = createTransactServer(
91
+ buildToolDefs(bundle),
92
+ ctx,
93
+ bundle.meta.bundleVersion
94
+ );
95
+ await server.connect(new StdioServerTransport());
96
+ console.error(
97
+ `[eps-transact-mcp] stdio ready (${environment}, bundle ${bundle.meta.bundleVersion})`
98
+ );
99
+ void notifyIfOutdated(OWN_VERSION);
100
+ };
101
+ main().catch((err) => {
102
+ console.error(
103
+ `[eps-transact-mcp] fatal: ${err instanceof Error ? err.message : String(err)}`
104
+ );
105
+ process.exit(1);
106
+ });
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@ekoindia/eps-transact-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Transactional MCP server for Eko Platform Services (EPS) verification APIs — remote (streamable HTTP) and local (stdio).",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/ekoindia/eps-platform.git",
10
+ "directory": "packages/eps-transact-mcp"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "bin": {
16
+ "eps-transact-mcp": "dist/stdio.js"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "data"
21
+ ],
22
+ "scripts": {
23
+ "bake": "node scripts/bake-bundle.mjs",
24
+ "build": "tsup",
25
+ "dev": "tsx watch src/index.ts",
26
+ "start": "node dist/index.js",
27
+ "test": "vitest run",
28
+ "typecheck": "tsc --noEmit",
29
+ "prepublishOnly": "npm run bake && npm run build"
30
+ },
31
+ "dependencies": {
32
+ "@ekoindia/eps-sdk": "*",
33
+ "@hono/node-server": "^1.0.0",
34
+ "@modelcontextprotocol/sdk": "^1.29.0",
35
+ "hono": "^4.0.0"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^22.20.0",
39
+ "tsup": "^8.0.0",
40
+ "tsx": "^4.22.4",
41
+ "typescript": "^5.8.3",
42
+ "vitest": "^3.2.4"
43
+ },
44
+ "engines": {
45
+ "node": ">=20"
46
+ }
47
+ }