@ekoindia/eps-context-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.
- package/README.md +171 -0
- package/data/eps.json +13867 -0
- package/dist/index.js +263 -0
- package/package.json +40 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
|
|
6
|
+
// src/load-bundle.ts
|
|
7
|
+
import { promises as fs } from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
var here = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
var BAKED_PATH = path.resolve(here, "../data/eps.json");
|
|
12
|
+
var loadBundle = async () => {
|
|
13
|
+
const url = process.env.EPS_BUNDLE_URL;
|
|
14
|
+
if (url) {
|
|
15
|
+
try {
|
|
16
|
+
const res = await fetch(url);
|
|
17
|
+
if (res.ok)
|
|
18
|
+
return { bundle: await res.json(), source: "remote" };
|
|
19
|
+
} catch {
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const raw = await fs.readFile(BAKED_PATH, "utf8");
|
|
23
|
+
return { bundle: JSON.parse(raw), source: "baked" };
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// src/server.ts
|
|
27
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
28
|
+
import { z } from "zod";
|
|
29
|
+
|
|
30
|
+
// src/bundle-access.ts
|
|
31
|
+
var toIndexEntry = (a) => ({
|
|
32
|
+
slug: a.slug,
|
|
33
|
+
productId: a.productId,
|
|
34
|
+
productName: a.productName,
|
|
35
|
+
name: a.name,
|
|
36
|
+
method: a.method,
|
|
37
|
+
path: a.path,
|
|
38
|
+
summary: a.summary,
|
|
39
|
+
category: a.category,
|
|
40
|
+
relevance: a.relevance
|
|
41
|
+
});
|
|
42
|
+
var listApis = (bundle, category) => bundle.apis.filter((a) => !category || a.category === category).map(toIndexEntry);
|
|
43
|
+
var listTopics = (bundle) => Object.keys(bundle.topics);
|
|
44
|
+
var listRecipes = (bundle) => bundle.recipes.map((r) => ({ id: r.id, name: r.name, summary: r.summary }));
|
|
45
|
+
var searchApis = (bundle, query) => {
|
|
46
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
47
|
+
if (!terms.length) return listApis(bundle);
|
|
48
|
+
const scored = bundle.apis.map((a) => {
|
|
49
|
+
const hay = `${a.name} ${a.summary} ${a.path} ${a.category} ${a.productName}`.toLowerCase();
|
|
50
|
+
let score = 0;
|
|
51
|
+
for (const t of terms) if (hay.includes(t)) score += 1;
|
|
52
|
+
return { a, score };
|
|
53
|
+
});
|
|
54
|
+
return scored.filter((s) => s.score > 0).sort((x, y) => y.score - x.score).map((s) => toIndexEntry(s.a));
|
|
55
|
+
};
|
|
56
|
+
var getApi = (bundle, slug) => bundle.apis.find((a) => a.slug === slug);
|
|
57
|
+
var getTopic = (bundle, topic) => bundle.topics[topic];
|
|
58
|
+
var getRecipe = (bundle, id) => bundle.recipes.find((r) => r.id === id);
|
|
59
|
+
|
|
60
|
+
// src/signing-snippets.ts
|
|
61
|
+
var SIGNING_LANGUAGES = [
|
|
62
|
+
"php",
|
|
63
|
+
"java",
|
|
64
|
+
"csharp",
|
|
65
|
+
"javascript",
|
|
66
|
+
"python",
|
|
67
|
+
"go"
|
|
68
|
+
];
|
|
69
|
+
var SNIPPETS = {
|
|
70
|
+
php: `<?php
|
|
71
|
+
// Backend only. Never expose access_key in a browser.
|
|
72
|
+
$accessKey = getenv('EKO_ACCESS_KEY');
|
|
73
|
+
$timestamp = (string) round(microtime(true) * 1000);
|
|
74
|
+
$encodedKey = base64_encode($accessKey);
|
|
75
|
+
$secretKey = base64_encode(hash_hmac('sha256', $timestamp, $encodedKey, true));
|
|
76
|
+
// Send headers: developer_key, secret-key: $secretKey, secret-key-timestamp: $timestamp
|
|
77
|
+
`,
|
|
78
|
+
java: `// Backend only. Never expose access_key in a browser.
|
|
79
|
+
import javax.crypto.Mac;
|
|
80
|
+
import javax.crypto.spec.SecretKeySpec;
|
|
81
|
+
import java.util.Base64;
|
|
82
|
+
|
|
83
|
+
String accessKey = System.getenv("EKO_ACCESS_KEY");
|
|
84
|
+
String timestamp = String.valueOf(System.currentTimeMillis());
|
|
85
|
+
String encodedKey = Base64.getEncoder().encodeToString(accessKey.getBytes("UTF-8"));
|
|
86
|
+
Mac mac = Mac.getInstance("HmacSHA256");
|
|
87
|
+
mac.init(new SecretKeySpec(encodedKey.getBytes("UTF-8"), "HmacSHA256"));
|
|
88
|
+
String secretKey = Base64.getEncoder().encodeToString(mac.doFinal(timestamp.getBytes("UTF-8")));
|
|
89
|
+
`,
|
|
90
|
+
csharp: `// Backend only. Never expose access_key in a browser.
|
|
91
|
+
using System;
|
|
92
|
+
using System.Security.Cryptography;
|
|
93
|
+
using System.Text;
|
|
94
|
+
|
|
95
|
+
string accessKey = Environment.GetEnvironmentVariable("EKO_ACCESS_KEY");
|
|
96
|
+
string timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString();
|
|
97
|
+
string encodedKey = Convert.ToBase64String(Encoding.UTF8.GetBytes(accessKey));
|
|
98
|
+
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(encodedKey));
|
|
99
|
+
string secretKey = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(timestamp)));
|
|
100
|
+
`,
|
|
101
|
+
javascript: `// Backend only (Node.js). Never expose access_key in a browser.
|
|
102
|
+
import crypto from "node:crypto";
|
|
103
|
+
|
|
104
|
+
const accessKey = process.env.EKO_ACCESS_KEY;
|
|
105
|
+
const timestamp = Date.now().toString();
|
|
106
|
+
const encodedKey = Buffer.from(accessKey).toString("base64");
|
|
107
|
+
const secretKey = crypto
|
|
108
|
+
.createHmac("sha256", encodedKey)
|
|
109
|
+
.update(timestamp)
|
|
110
|
+
.digest("base64");
|
|
111
|
+
// headers: { developer_key, "secret-key": secretKey, "secret-key-timestamp": timestamp }
|
|
112
|
+
`,
|
|
113
|
+
python: `# Backend only. Never expose access_key in a browser.
|
|
114
|
+
import base64, hashlib, hmac, os, time
|
|
115
|
+
|
|
116
|
+
access_key = os.environ["EKO_ACCESS_KEY"]
|
|
117
|
+
timestamp = str(int(time.time() * 1000))
|
|
118
|
+
encoded_key = base64.b64encode(access_key.encode())
|
|
119
|
+
secret_key = base64.b64encode(
|
|
120
|
+
hmac.new(encoded_key, timestamp.encode(), hashlib.sha256).digest()
|
|
121
|
+
).decode()
|
|
122
|
+
`,
|
|
123
|
+
go: `// Backend only. Never expose access_key in a browser.
|
|
124
|
+
package main
|
|
125
|
+
|
|
126
|
+
import (
|
|
127
|
+
"crypto/hmac"
|
|
128
|
+
"crypto/sha256"
|
|
129
|
+
"encoding/base64"
|
|
130
|
+
"fmt"
|
|
131
|
+
"os"
|
|
132
|
+
"time"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
func secretKey() (string, string) {
|
|
136
|
+
accessKey := os.Getenv("EKO_ACCESS_KEY")
|
|
137
|
+
timestamp := fmt.Sprintf("%d", time.Now().UnixMilli())
|
|
138
|
+
encodedKey := base64.StdEncoding.EncodeToString([]byte(accessKey))
|
|
139
|
+
mac := hmac.New(sha256.New, []byte(encodedKey))
|
|
140
|
+
mac.Write([]byte(timestamp))
|
|
141
|
+
return base64.StdEncoding.EncodeToString(mac.Sum(nil)), timestamp
|
|
142
|
+
}
|
|
143
|
+
`
|
|
144
|
+
};
|
|
145
|
+
var getSigningSnippet = (language) => SNIPPETS[language] ?? `Unsupported language "${language}". Supported: ${SIGNING_LANGUAGES.join(", ")}.`;
|
|
146
|
+
|
|
147
|
+
// src/server.ts
|
|
148
|
+
var json = (value) => ({
|
|
149
|
+
content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
|
|
150
|
+
});
|
|
151
|
+
var createEpsServer = (bundle, source) => {
|
|
152
|
+
const server = new McpServer({
|
|
153
|
+
name: "eps-context-mcp",
|
|
154
|
+
version: bundle.meta.bundleVersion
|
|
155
|
+
});
|
|
156
|
+
server.registerTool(
|
|
157
|
+
"list_apis",
|
|
158
|
+
{
|
|
159
|
+
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() }
|
|
162
|
+
},
|
|
163
|
+
async ({ category }) => json(listApis(bundle, category))
|
|
164
|
+
);
|
|
165
|
+
server.registerTool(
|
|
166
|
+
"list_topics",
|
|
167
|
+
{
|
|
168
|
+
title: "List topics",
|
|
169
|
+
description: "List documentation topic ids.",
|
|
170
|
+
inputSchema: {}
|
|
171
|
+
},
|
|
172
|
+
async () => json(listTopics(bundle))
|
|
173
|
+
);
|
|
174
|
+
server.registerTool(
|
|
175
|
+
"list_recipes",
|
|
176
|
+
{
|
|
177
|
+
title: "List recipes",
|
|
178
|
+
description: "List multi-step recipe ids + names.",
|
|
179
|
+
inputSchema: {}
|
|
180
|
+
},
|
|
181
|
+
async () => json(listRecipes(bundle))
|
|
182
|
+
);
|
|
183
|
+
server.registerTool(
|
|
184
|
+
"search",
|
|
185
|
+
{
|
|
186
|
+
title: "Search APIs",
|
|
187
|
+
description: "Ranked endpoint matches for a query (ids only, no bodies).",
|
|
188
|
+
inputSchema: { query: z.string() }
|
|
189
|
+
},
|
|
190
|
+
async ({ query }) => json(searchApis(bundle, query))
|
|
191
|
+
);
|
|
192
|
+
server.registerTool(
|
|
193
|
+
"get_api",
|
|
194
|
+
{
|
|
195
|
+
title: "Get API detail",
|
|
196
|
+
description: "Full detail for one endpoint by slug.",
|
|
197
|
+
inputSchema: { slug: z.string() }
|
|
198
|
+
},
|
|
199
|
+
async ({ slug }) => {
|
|
200
|
+
const api = getApi(bundle, slug);
|
|
201
|
+
return api ? json(api) : json({ error: `Unknown slug "${slug}"` });
|
|
202
|
+
}
|
|
203
|
+
);
|
|
204
|
+
server.registerTool(
|
|
205
|
+
"get_topic",
|
|
206
|
+
{
|
|
207
|
+
title: "Get topic",
|
|
208
|
+
description: "One topic: auth | errors | pricing | environments.",
|
|
209
|
+
inputSchema: {
|
|
210
|
+
topic: z.enum(["auth", "errors", "pricing", "environments"])
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
async ({ topic }) => json(getTopic(bundle, topic))
|
|
214
|
+
);
|
|
215
|
+
server.registerTool(
|
|
216
|
+
"get_recipe",
|
|
217
|
+
{
|
|
218
|
+
title: "Get recipe",
|
|
219
|
+
description: "One multi-step recipe (steps + branches) by id.",
|
|
220
|
+
inputSchema: { id: z.string() }
|
|
221
|
+
},
|
|
222
|
+
async ({ id }) => {
|
|
223
|
+
const recipe = getRecipe(bundle, id);
|
|
224
|
+
return recipe ? json(recipe) : json({ error: `Unknown recipe "${id}"` });
|
|
225
|
+
}
|
|
226
|
+
);
|
|
227
|
+
server.registerTool(
|
|
228
|
+
"get_signing_snippet",
|
|
229
|
+
{
|
|
230
|
+
title: "Get signing snippet",
|
|
231
|
+
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) }
|
|
233
|
+
},
|
|
234
|
+
async ({ language }) => ({
|
|
235
|
+
content: [{ type: "text", text: getSigningSnippet(language) }]
|
|
236
|
+
})
|
|
237
|
+
);
|
|
238
|
+
server.registerTool(
|
|
239
|
+
"get_meta",
|
|
240
|
+
{
|
|
241
|
+
title: "Get meta",
|
|
242
|
+
description: "Bundle org/version + data source.",
|
|
243
|
+
inputSchema: {}
|
|
244
|
+
},
|
|
245
|
+
async () => json({ ...bundle.meta, source })
|
|
246
|
+
);
|
|
247
|
+
return server;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// src/index.ts
|
|
251
|
+
async function main() {
|
|
252
|
+
const { bundle, source } = await loadBundle();
|
|
253
|
+
const server = createEpsServer(bundle, source);
|
|
254
|
+
const transport = new StdioServerTransport();
|
|
255
|
+
await server.connect(transport);
|
|
256
|
+
console.error(
|
|
257
|
+
`eps-context-mcp running (bundle ${bundle.meta.bundleVersion}, ${source})`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
main().catch((err) => {
|
|
261
|
+
console.error("eps-context-mcp fatal:", err);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ekoindia/eps-context-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local MCP server giving AI coding agents context for Eko Platform Services (EPS) APIs.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/ekoindia/eko-eps-website.git",
|
|
10
|
+
"directory": "packages/eps-context-mcp"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"eps-context-mcp": "dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"data"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"bake": "node scripts/bake-bundle.mjs",
|
|
24
|
+
"build": "tsup",
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"prepublishOnly": "npm run bake && npm run build"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
30
|
+
"zod": "^3.25.76"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"tsup": "^8.0.0",
|
|
34
|
+
"typescript": "^5.8.3",
|
|
35
|
+
"vitest": "^3.2.4"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
}
|
|
40
|
+
}
|