peptiko-mcp-server 1.0.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/src/index.ts ADDED
@@ -0,0 +1,317 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { PRODUCTS, SHIPPING_ZONES, type Product } from "./data.js";
6
+
7
+ const server = new McpServer({
8
+ name: "peptiko-mcp",
9
+ version: "1.0.0",
10
+ description:
11
+ "Peptiko — research-grade peptide catalog for Moldova, CIS & EU. Search products, check pricing, calculate reconstitution doses, and view shipping info.",
12
+ });
13
+
14
+ /* ── TOOL: search_peptides ── */
15
+ server.tool(
16
+ "search_peptides",
17
+ "Search the Peptiko peptide catalog by name, category, or keyword. Returns matching products with price, purity, and description.",
18
+ {
19
+ query: z
20
+ .string()
21
+ .optional()
22
+ .describe("Search term — compound name, abbreviation, or keyword (e.g. 'BPC-157', 'recovery', 'fat loss')"),
23
+ category: z
24
+ .enum(["recovery", "metabolic", "performance", "longevity", "all"])
25
+ .optional()
26
+ .describe("Filter by category"),
27
+ max_results: z
28
+ .number()
29
+ .min(1)
30
+ .max(50)
31
+ .optional()
32
+ .describe("Maximum results to return (default 10)"),
33
+ },
34
+ async ({ query, category, max_results }) => {
35
+ let results = [...PRODUCTS];
36
+
37
+ if (category && category !== "all") {
38
+ results = results.filter((p) => p.category === category);
39
+ }
40
+
41
+ if (query) {
42
+ const q = query.toLowerCase();
43
+ results = results.filter(
44
+ (p) =>
45
+ p.name.toLowerCase().includes(q) ||
46
+ p.abbr.toLowerCase().includes(q) ||
47
+ p.id.includes(q) ||
48
+ p.desc.toLowerCase().includes(q) ||
49
+ p.searchTerms.toLowerCase().includes(q)
50
+ );
51
+ }
52
+
53
+ const limit = max_results ?? 10;
54
+ results = results.slice(0, limit);
55
+
56
+ if (results.length === 0) {
57
+ return {
58
+ content: [
59
+ {
60
+ type: "text" as const,
61
+ text: `No peptides found matching "${query || ""}". Try a different search term or browse all with category "all".`,
62
+ },
63
+ ],
64
+ };
65
+ }
66
+
67
+ const formatted = results
68
+ .map(
69
+ (p) =>
70
+ `**${p.name}** (${p.abbr}) — €${p.price}\n` +
71
+ ` Category: ${p.category} | Purity: ${p.purity}\n` +
72
+ ` Unit: ${p.unit}\n` +
73
+ ` ${p.desc}\n` +
74
+ ` CAS: ${p.cas} | Storage: ${p.storage}\n` +
75
+ ` Formats: ${p.formats.join(", ")}`
76
+ )
77
+ .join("\n\n");
78
+
79
+ return {
80
+ content: [
81
+ {
82
+ type: "text" as const,
83
+ text: `Found ${results.length} peptide${results.length !== 1 ? "s" : ""}:\n\n${formatted}`,
84
+ },
85
+ ],
86
+ };
87
+ }
88
+ );
89
+
90
+ /* ── TOOL: get_product_details ── */
91
+ server.tool(
92
+ "get_product_details",
93
+ "Get full details for a specific peptide including mechanism of action, storage, CAS number, available formats, and pricing.",
94
+ {
95
+ product_id: z
96
+ .string()
97
+ .describe("Product ID or abbreviation (e.g. 'bpc-tb', 'GHK-Cu', 'semaglutide')"),
98
+ },
99
+ async ({ product_id }) => {
100
+ const q = product_id.toLowerCase();
101
+ const product = PRODUCTS.find(
102
+ (p) =>
103
+ p.id === q ||
104
+ p.abbr.toLowerCase() === q ||
105
+ p.name.toLowerCase().includes(q)
106
+ );
107
+
108
+ if (!product) {
109
+ return {
110
+ content: [
111
+ {
112
+ type: "text" as const,
113
+ text: `Product "${product_id}" not found. Use search_peptides to browse the catalog.`,
114
+ },
115
+ ],
116
+ };
117
+ }
118
+
119
+ const detail =
120
+ `# ${product.name} "${product.nick}"\n\n` +
121
+ `**Category:** ${product.category}\n` +
122
+ `**Price:** €${product.price} per ${product.unit}\n` +
123
+ `**Purity:** ${product.purity}\n` +
124
+ `**CAS:** ${product.cas}\n` +
125
+ `**Storage:** ${product.storage}\n\n` +
126
+ `## Description\n${product.desc}\n\n` +
127
+ `## Mechanism of Action\n${product.mechanism}\n\n` +
128
+ `## Available Formats\n${product.formats.map((f) => `- ${f}`).join("\n")}\n\n` +
129
+ `---\n*All products are for research use only. Not for human consumption.*`;
130
+
131
+ return {
132
+ content: [{ type: "text" as const, text: detail }],
133
+ };
134
+ }
135
+ );
136
+
137
+ /* ── TOOL: reconstitution_calculator ── */
138
+ server.tool(
139
+ "reconstitution_calculator",
140
+ "Calculate peptide reconstitution — draw volume, syringe units, and doses per vial based on vial size, water volume, and desired dose.",
141
+ {
142
+ vial_mg: z.number().positive().describe("Peptide vial size in milligrams"),
143
+ water_ml: z
144
+ .number()
145
+ .positive()
146
+ .describe("Bacteriostatic water added in milliliters"),
147
+ dose_mcg: z
148
+ .number()
149
+ .positive()
150
+ .describe("Desired dose in micrograms"),
151
+ syringe_ml: z
152
+ .number()
153
+ .positive()
154
+ .optional()
155
+ .describe("Syringe size in ml — 0.3, 0.5, or 1.0 (default 0.5)"),
156
+ },
157
+ async ({ vial_mg, water_ml, dose_mcg, syringe_ml }) => {
158
+ const syringe = syringe_ml ?? 0.5;
159
+ const syringeUnits = syringe * 100; // 0.3ml = 30u, 0.5ml = 50u, 1.0ml = 100u
160
+ const concentration = (vial_mg * 1000) / water_ml; // mcg/ml
161
+ const drawMl = dose_mcg / concentration;
162
+ const drawUnits = drawMl * (syringeUnits / syringe);
163
+ const dosesPerVial = Math.floor((vial_mg * 1000) / dose_mcg);
164
+ const overflow = drawMl > syringe;
165
+
166
+ const result =
167
+ `## Reconstitution Calculation\n\n` +
168
+ `| Parameter | Value |\n|---|---|\n` +
169
+ `| Vial size | ${vial_mg} mg |\n` +
170
+ `| BAC water | ${water_ml} ml |\n` +
171
+ `| Concentration | ${concentration.toFixed(0)} mcg/ml |\n` +
172
+ `| Desired dose | ${dose_mcg} mcg |\n` +
173
+ `| **Draw volume** | **${drawMl.toFixed(4)} ml** |\n` +
174
+ `| **Syringe units** | **${drawUnits.toFixed(1)} units** (${syringe}ml / ${syringeUnits}u syringe) |\n` +
175
+ `| Doses per vial | ${dosesPerVial} |\n` +
176
+ (overflow
177
+ ? `\n⚠️ **Warning:** Draw volume exceeds syringe capacity (${syringe} ml). Use a larger syringe or reduce the dose.\n`
178
+ : "") +
179
+ `\n*For research use only. Always verify calculations.*`;
180
+
181
+ return {
182
+ content: [{ type: "text" as const, text: result }],
183
+ };
184
+ }
185
+ );
186
+
187
+ /* ── TOOL: shipping_info ── */
188
+ server.tool(
189
+ "shipping_info",
190
+ "Get Peptiko shipping rates, delivery times, and available regions. Peptiko ships from Moldova to CIS and EU countries.",
191
+ {
192
+ country: z
193
+ .string()
194
+ .optional()
195
+ .describe("Country name or code (e.g. 'Moldova', 'MD', 'Romania', 'EU')"),
196
+ },
197
+ async ({ country }) => {
198
+ if (country) {
199
+ const q = country.toLowerCase();
200
+ const zone = SHIPPING_ZONES.find(
201
+ (z) =>
202
+ z.code.toLowerCase() === q ||
203
+ z.label.toLowerCase().includes(q) ||
204
+ z.countries.some((c) => c.toLowerCase().includes(q))
205
+ );
206
+
207
+ if (zone) {
208
+ return {
209
+ content: [
210
+ {
211
+ type: "text" as const,
212
+ text:
213
+ `## Shipping to ${zone.label}\n\n` +
214
+ `- **Cost:** ${zone.cost === 0 ? "Free" : `€${zone.cost}`}\n` +
215
+ `- **Delivery:** ${zone.days}\n` +
216
+ `- **Countries:** ${zone.countries.join(", ")}\n\n` +
217
+ `All orders include tracking. Temperature-sensitive items ship with cold packs.`,
218
+ },
219
+ ],
220
+ };
221
+ }
222
+ }
223
+
224
+ const allZones = SHIPPING_ZONES.map(
225
+ (z) =>
226
+ `**${z.label}** — ${z.cost === 0 ? "Free" : `€${z.cost}`} (${z.days})\n ${z.countries.join(", ")}`
227
+ ).join("\n\n");
228
+
229
+ return {
230
+ content: [
231
+ {
232
+ type: "text" as const,
233
+ text: `## Peptiko Shipping Zones\n\n${allZones}\n\n*All orders include tracking. Ships from Chișinău, Moldova.*`,
234
+ },
235
+ ],
236
+ };
237
+ }
238
+ );
239
+
240
+ /* ── TOOL: list_categories ── */
241
+ server.tool(
242
+ "list_categories",
243
+ "List all peptide categories and the number of products in each.",
244
+ {},
245
+ async () => {
246
+ const cats: Record<string, number> = {};
247
+ for (const p of PRODUCTS) {
248
+ cats[p.category] = (cats[p.category] || 0) + 1;
249
+ }
250
+ const lines = Object.entries(cats)
251
+ .sort((a, b) => b[1] - a[1])
252
+ .map(([cat, count]) => `- **${cat}**: ${count} products`);
253
+
254
+ return {
255
+ content: [
256
+ {
257
+ type: "text" as const,
258
+ text: `## Peptiko Catalog Categories\n\nTotal: ${PRODUCTS.length} peptides\n\n${lines.join("\n")}\n\nUse \`search_peptides\` with a category filter to browse.`,
259
+ },
260
+ ],
261
+ };
262
+ }
263
+ );
264
+
265
+ /* ── RESOURCE: catalog summary ── */
266
+ server.resource(
267
+ "peptiko://catalog",
268
+ "Complete Peptiko peptide catalog — all products with prices and descriptions",
269
+ async () => {
270
+ const catalog = PRODUCTS.map(
271
+ (p) => `${p.name} (${p.abbr}) | €${p.price} | ${p.category} | ${p.purity} | ${p.desc}`
272
+ ).join("\n");
273
+
274
+ return {
275
+ contents: [
276
+ {
277
+ uri: "peptiko://catalog",
278
+ mimeType: "text/plain",
279
+ text: `Peptiko Research Peptide Catalog — ${PRODUCTS.length} products\nAll products ship from Chișinău, Moldova. Research use only.\n\n${catalog}`,
280
+ },
281
+ ],
282
+ };
283
+ }
284
+ );
285
+
286
+ /* ── RESOURCE: compliance ── */
287
+ server.resource(
288
+ "peptiko://compliance",
289
+ "Peptiko RUO compliance and legal disclaimers",
290
+ async () => {
291
+ return {
292
+ contents: [
293
+ {
294
+ uri: "peptiko://compliance",
295
+ mimeType: "text/plain",
296
+ text:
297
+ "RESEARCH USE ONLY — PEPTIKO COMPLIANCE NOTICE\n\n" +
298
+ "All products sold by Peptiko are intended strictly for laboratory research, " +
299
+ "in-vitro testing, and educational purposes only.\n\n" +
300
+ "Peptiko does not promote, condone, or support the use of any product for human " +
301
+ "or animal consumption, therapeutic application, or any purpose other than lawful research.\n\n" +
302
+ "All sales are final. No returns or refunds. See peptiko.xyz/terms for full terms.\n\n" +
303
+ "Contact: peptiko@protonmail.com\nWebsite: https://peptiko.xyz",
304
+ },
305
+ ],
306
+ };
307
+ }
308
+ );
309
+
310
+ /* ── START ── */
311
+ async function main() {
312
+ const transport = new StdioServerTransport();
313
+ await server.connect(transport);
314
+ console.error("Peptiko MCP server started");
315
+ }
316
+
317
+ main().catch(console.error);
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "lib": ["ES2020"],
6
+ "moduleResolution": "bundler",
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "resolveJsonModule": true,
14
+ "declaration": true,
15
+ "sourceMap": true
16
+ },
17
+ "include": ["src/**/*"],
18
+ "exclude": ["node_modules", "dist"]
19
+ }