@universal-mcp-toolkit/server-redis 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,35 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/json",
3
+ "name": "redis",
4
+ "title": "Redis MCP Server",
5
+ "description": "Key inspection and cache diagnostics tools for Redis.",
6
+ "version": "0.1.0",
7
+ "transports": [
8
+ "stdio",
9
+ "http+sse"
10
+ ],
11
+ "authentication": {
12
+ "mode": "environment-variables",
13
+ "required": [
14
+ "REDIS_URL"
15
+ ]
16
+ },
17
+ "capabilities": {
18
+ "tools": true,
19
+ "resources": true,
20
+ "prompts": true
21
+ },
22
+ "packageName": "@universal-mcp-toolkit/server-redis",
23
+ "homepage": "https://github.com/universal-mcp-toolkit/universal-mcp-toolkit#readme",
24
+ "tools": [
25
+ "get-key",
26
+ "set-key",
27
+ "inspect-server-info"
28
+ ],
29
+ "resources": [
30
+ "cache-overview"
31
+ ],
32
+ "prompts": [
33
+ "cache-debug"
34
+ ]
35
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 universal-mcp-toolkit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,78 @@
1
+ import * as _universal_mcp_toolkit_core from '@universal-mcp-toolkit/core';
2
+ import { ToolkitServer, ToolkitServerMetadata } from '@universal-mcp-toolkit/core';
3
+ import { z } from 'zod';
4
+
5
+ type JsonPrimitive = boolean | number | string | null;
6
+ interface JsonObject {
7
+ [key: string]: JsonValue;
8
+ }
9
+ type JsonValue = JsonPrimitive | JsonObject | JsonValue[];
10
+ declare const redisEnvShape: {
11
+ REDIS_URL: z.ZodString;
12
+ REDIS_ALLOW_WRITES: z.ZodPipe<z.ZodDefault<z.ZodEnum<{
13
+ true: "true";
14
+ false: "false";
15
+ }>>, z.ZodTransform<boolean, "true" | "false">>;
16
+ REDIS_DEFAULT_TTL_SECONDS: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
17
+ REDIS_VALUE_SAMPLE_LIMIT: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
18
+ REDIS_RESOURCE_SCAN_PATTERN: z.ZodDefault<z.ZodString>;
19
+ REDIS_RESOURCE_KEY_LIMIT: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
20
+ };
21
+ type RedisEnv = z.infer<z.ZodObject<typeof redisEnvShape>>;
22
+ interface RedisInfoProperty {
23
+ name: string;
24
+ value: string;
25
+ }
26
+ interface RedisKeyInspection {
27
+ key: string;
28
+ exists: boolean;
29
+ keyType: string | null;
30
+ ttlSeconds: number | null;
31
+ value: JsonValue | null;
32
+ preview: string;
33
+ size: number | null;
34
+ }
35
+ interface RedisClient {
36
+ getKey(input: {
37
+ key: string;
38
+ sampleSize: number;
39
+ }): Promise<RedisKeyInspection>;
40
+ setKey(input: {
41
+ key: string;
42
+ serializedValue: string;
43
+ ttlSeconds: number | null;
44
+ onlyIfAbsent: boolean;
45
+ }): Promise<{
46
+ key: string;
47
+ stored: boolean;
48
+ ttlSeconds: number | null;
49
+ }>;
50
+ inspectServerInfo(input: {
51
+ section?: string;
52
+ }): Promise<{
53
+ section: string;
54
+ properties: RedisInfoProperty[];
55
+ raw: string;
56
+ }>;
57
+ listKeys(input: {
58
+ pattern: string;
59
+ limit: number;
60
+ }): Promise<string[]>;
61
+ close?(): Promise<void>;
62
+ }
63
+ declare const metadata: ToolkitServerMetadata;
64
+ declare const serverCard: _universal_mcp_toolkit_core.ToolkitServerCard;
65
+ declare class RedisServer extends ToolkitServer {
66
+ private readonly env;
67
+ private readonly client;
68
+ constructor(env: RedisEnv, client: RedisClient);
69
+ close(): Promise<void>;
70
+ }
71
+ interface CreateRedisServerOptions {
72
+ env?: RedisEnv;
73
+ client?: RedisClient;
74
+ }
75
+ declare function createServer(options?: CreateRedisServerOptions): RedisServer;
76
+ declare function main(argv?: readonly string[]): Promise<void>;
77
+
78
+ export { type CreateRedisServerOptions, type JsonObject, type JsonValue, type RedisClient, type RedisEnv, type RedisInfoProperty, type RedisKeyInspection, RedisServer, createServer, main, metadata, serverCard };
package/dist/index.js ADDED
@@ -0,0 +1,587 @@
1
+ // src/index.ts
2
+ import { pathToFileURL } from "url";
3
+ import {
4
+ ExternalServiceError,
5
+ ToolkitServer,
6
+ ValidationError,
7
+ createServerCard,
8
+ defineTool,
9
+ loadEnv,
10
+ parseRuntimeOptions,
11
+ runToolkitServer
12
+ } from "@universal-mcp-toolkit/core";
13
+ import { createClient } from "redis";
14
+ import { z } from "zod";
15
+ var jsonValueSchema = z.lazy(
16
+ () => z.union([z.string(), z.number().finite(), z.boolean(), z.null(), z.array(jsonValueSchema), z.record(z.string(), jsonValueSchema)])
17
+ );
18
+ var jsonObjectSchema = z.record(z.string(), jsonValueSchema);
19
+ var booleanFlag = z.enum(["true", "false"]).default("false").transform((value) => value === "true");
20
+ var redisEnvShape = {
21
+ REDIS_URL: z.string().min(1),
22
+ REDIS_ALLOW_WRITES: booleanFlag,
23
+ REDIS_DEFAULT_TTL_SECONDS: z.coerce.number().int().nonnegative().max(604800).default(0),
24
+ REDIS_VALUE_SAMPLE_LIMIT: z.coerce.number().int().positive().max(100).default(10),
25
+ REDIS_RESOURCE_SCAN_PATTERN: z.string().min(1).default("*"),
26
+ REDIS_RESOURCE_KEY_LIMIT: z.coerce.number().int().positive().max(100).default(20)
27
+ };
28
+ var TOOL_NAMES = ["get-key", "inspect-server-info", "set-key"];
29
+ var RESOURCE_NAMES = ["cache-overview"];
30
+ var PROMPT_NAMES = ["cache-debug"];
31
+ var metadata = {
32
+ id: "redis",
33
+ title: "Redis MCP Server",
34
+ description: "Key inspection and cache diagnostics tools for Redis.",
35
+ version: "0.1.0",
36
+ packageName: "@universal-mcp-toolkit/server-redis",
37
+ homepage: "https://github.com/universal-mcp-toolkit/universal-mcp-toolkit#readme",
38
+ repositoryUrl: "https://github.com/universal-mcp-toolkit/universal-mcp-toolkit",
39
+ envVarNames: ["REDIS_URL"],
40
+ transports: ["stdio", "sse"],
41
+ toolNames: TOOL_NAMES,
42
+ resourceNames: RESOURCE_NAMES,
43
+ promptNames: PROMPT_NAMES
44
+ };
45
+ var serverCard = createServerCard(metadata);
46
+ function extractErrorMessage(error) {
47
+ if (error instanceof Error) {
48
+ return error.message;
49
+ }
50
+ return "Unknown Redis error.";
51
+ }
52
+ function sanitizeJsonValue(value) {
53
+ if (value === null || typeof value === "string" || typeof value === "boolean") {
54
+ return value;
55
+ }
56
+ if (typeof value === "number") {
57
+ return Number.isFinite(value) ? value : String(value);
58
+ }
59
+ if (typeof value === "bigint") {
60
+ return value.toString();
61
+ }
62
+ if (Array.isArray(value)) {
63
+ return value.map((entry) => sanitizeJsonValue(entry));
64
+ }
65
+ if (value instanceof Date) {
66
+ return value.toISOString();
67
+ }
68
+ if (value instanceof Uint8Array) {
69
+ return Buffer.from(value).toString("base64");
70
+ }
71
+ if (typeof value === "object") {
72
+ const result = {};
73
+ for (const [key, entry] of Object.entries(value)) {
74
+ result[key] = sanitizeJsonValue(entry);
75
+ }
76
+ return result;
77
+ }
78
+ return String(value);
79
+ }
80
+ function maskRedisUrl(urlValue) {
81
+ try {
82
+ const url = new URL(urlValue);
83
+ if (url.username || url.password) {
84
+ url.username = "***";
85
+ url.password = "***";
86
+ }
87
+ return url.toString();
88
+ } catch {
89
+ return urlValue.replace(/\/\/[^@/]+@/u, "//***:***@");
90
+ }
91
+ }
92
+ function truncateText(value, maxLength = 120) {
93
+ if (value.length <= maxLength) {
94
+ return value;
95
+ }
96
+ return `${value.slice(0, maxLength - 3)}...`;
97
+ }
98
+ function parseInteger(reply) {
99
+ if (typeof reply === "number" && Number.isFinite(reply)) {
100
+ return reply;
101
+ }
102
+ if (typeof reply === "string" && reply.length > 0) {
103
+ const parsed = Number(reply);
104
+ return Number.isFinite(parsed) ? parsed : null;
105
+ }
106
+ return null;
107
+ }
108
+ function parseStringArray(reply) {
109
+ if (!Array.isArray(reply)) {
110
+ return [];
111
+ }
112
+ return reply.map((entry) => String(entry));
113
+ }
114
+ function parseHashReply(reply) {
115
+ if (reply !== null && typeof reply === "object" && !Array.isArray(reply)) {
116
+ const result2 = {};
117
+ for (const [key, value] of Object.entries(reply)) {
118
+ result2[key] = sanitizeJsonValue(value);
119
+ }
120
+ return result2;
121
+ }
122
+ if (!Array.isArray(reply)) {
123
+ return {};
124
+ }
125
+ const result = {};
126
+ for (let index = 0; index < reply.length; index += 2) {
127
+ const key = reply[index];
128
+ const value = reply[index + 1];
129
+ if (key !== void 0) {
130
+ result[String(key)] = sanitizeJsonValue(value ?? null);
131
+ }
132
+ }
133
+ return result;
134
+ }
135
+ function parseZsetReply(reply) {
136
+ if (!Array.isArray(reply)) {
137
+ return [];
138
+ }
139
+ if (reply.every((entry) => Array.isArray(entry) && entry.length >= 2)) {
140
+ return reply.map((entry) => {
141
+ const pair = entry;
142
+ return {
143
+ member: String(pair[0]),
144
+ score: parseInteger(pair[1]) ?? String(pair[1] ?? "")
145
+ };
146
+ });
147
+ }
148
+ const result = [];
149
+ for (let index = 0; index < reply.length; index += 2) {
150
+ const member = reply[index];
151
+ const score = reply[index + 1];
152
+ if (member !== void 0) {
153
+ result.push({
154
+ member: String(member),
155
+ score: parseInteger(score) ?? String(score ?? "")
156
+ });
157
+ }
158
+ }
159
+ return result;
160
+ }
161
+ function parseStreamReply(reply) {
162
+ if (!Array.isArray(reply)) {
163
+ return [];
164
+ }
165
+ return reply.map((entry) => {
166
+ if (!Array.isArray(entry) || entry.length < 2) {
167
+ return {
168
+ raw: sanitizeJsonValue(entry)
169
+ };
170
+ }
171
+ const id = String(entry[0]);
172
+ const fields = parseHashReply(entry[1]);
173
+ return {
174
+ id,
175
+ fields
176
+ };
177
+ });
178
+ }
179
+ function parseInfoSection(raw) {
180
+ return raw.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#") && line.includes(":")).map((line) => {
181
+ const separatorIndex = line.indexOf(":");
182
+ return {
183
+ name: line.slice(0, separatorIndex),
184
+ value: line.slice(separatorIndex + 1)
185
+ };
186
+ });
187
+ }
188
+ function parseScanReply(reply) {
189
+ if (!Array.isArray(reply) || reply.length < 2) {
190
+ return {
191
+ cursor: "0",
192
+ keys: []
193
+ };
194
+ }
195
+ const cursor = String(reply[0]);
196
+ const keys = parseStringArray(reply[1]);
197
+ return {
198
+ cursor,
199
+ keys
200
+ };
201
+ }
202
+ var NodeRedisClient = class {
203
+ client;
204
+ constructor(url) {
205
+ this.client = createClient({ url });
206
+ }
207
+ async close() {
208
+ if (!this.client.isOpen) {
209
+ return;
210
+ }
211
+ await this.client.quit();
212
+ }
213
+ async getKey(input) {
214
+ const keyType = await this.execute(["TYPE", input.key]);
215
+ if (keyType === "none") {
216
+ return {
217
+ key: input.key,
218
+ exists: false,
219
+ keyType: null,
220
+ ttlSeconds: null,
221
+ value: null,
222
+ preview: "Key not found.",
223
+ size: null
224
+ };
225
+ }
226
+ const ttlReply = await this.execute(["TTL", input.key]);
227
+ const ttl = parseInteger(ttlReply);
228
+ const ttlSeconds = ttl === null || ttl < 0 ? null : ttl;
229
+ switch (keyType) {
230
+ case "string": {
231
+ const value = await this.execute(["GET", input.key]);
232
+ return {
233
+ key: input.key,
234
+ exists: true,
235
+ keyType,
236
+ ttlSeconds,
237
+ value,
238
+ preview: typeof value === "string" ? truncateText(value) : "null",
239
+ size: typeof value === "string" ? value.length : 0
240
+ };
241
+ }
242
+ case "hash": {
243
+ const value = parseHashReply(await this.execute(["HGETALL", input.key]));
244
+ return {
245
+ key: input.key,
246
+ exists: true,
247
+ keyType,
248
+ ttlSeconds,
249
+ value,
250
+ preview: `${Object.keys(value).length} hash field(s)`,
251
+ size: Object.keys(value).length
252
+ };
253
+ }
254
+ case "list": {
255
+ const value = parseStringArray(await this.execute(["LRANGE", input.key, "0", String(input.sampleSize - 1)]));
256
+ return {
257
+ key: input.key,
258
+ exists: true,
259
+ keyType,
260
+ ttlSeconds,
261
+ value,
262
+ preview: `${value.length} sampled list item(s)`,
263
+ size: value.length
264
+ };
265
+ }
266
+ case "set": {
267
+ const members = parseStringArray(await this.execute(["SMEMBERS", input.key])).slice(0, input.sampleSize);
268
+ return {
269
+ key: input.key,
270
+ exists: true,
271
+ keyType,
272
+ ttlSeconds,
273
+ value: members,
274
+ preview: `${members.length} sampled set member(s)`,
275
+ size: members.length
276
+ };
277
+ }
278
+ case "zset": {
279
+ const value = parseZsetReply(
280
+ await this.execute(["ZRANGE", input.key, "0", String(input.sampleSize - 1), "WITHSCORES"])
281
+ );
282
+ return {
283
+ key: input.key,
284
+ exists: true,
285
+ keyType,
286
+ ttlSeconds,
287
+ value,
288
+ preview: `${value.length} sampled sorted-set member(s)`,
289
+ size: value.length
290
+ };
291
+ }
292
+ case "stream": {
293
+ const value = parseStreamReply(
294
+ await this.execute(["XRANGE", input.key, "-", "+", "COUNT", String(input.sampleSize)])
295
+ );
296
+ return {
297
+ key: input.key,
298
+ exists: true,
299
+ keyType,
300
+ ttlSeconds,
301
+ value,
302
+ preview: `${value.length} sampled stream entr${value.length === 1 ? "y" : "ies"}`,
303
+ size: value.length
304
+ };
305
+ }
306
+ default:
307
+ return {
308
+ key: input.key,
309
+ exists: true,
310
+ keyType,
311
+ ttlSeconds,
312
+ value: null,
313
+ preview: `Key type '${keyType}' is not directly decoded by this server.`,
314
+ size: null
315
+ };
316
+ }
317
+ }
318
+ async setKey(input) {
319
+ const args = ["SET", input.key, input.serializedValue];
320
+ if (input.ttlSeconds !== null) {
321
+ args.push("EX", String(input.ttlSeconds));
322
+ }
323
+ if (input.onlyIfAbsent) {
324
+ args.push("NX");
325
+ }
326
+ const response = await this.execute(args);
327
+ return {
328
+ key: input.key,
329
+ stored: response === "OK",
330
+ ttlSeconds: input.ttlSeconds
331
+ };
332
+ }
333
+ async inspectServerInfo(input) {
334
+ const raw = await this.execute(input.section ? ["INFO", input.section] : ["INFO"]);
335
+ return {
336
+ section: input.section ?? "default",
337
+ properties: parseInfoSection(raw),
338
+ raw
339
+ };
340
+ }
341
+ async listKeys(input) {
342
+ const keys = [];
343
+ let cursor = "0";
344
+ do {
345
+ const reply = await this.execute(["SCAN", cursor, "MATCH", input.pattern, "COUNT", String(input.limit)]);
346
+ const parsed = parseScanReply(reply);
347
+ cursor = parsed.cursor;
348
+ for (const key of parsed.keys) {
349
+ if (keys.length >= input.limit) {
350
+ break;
351
+ }
352
+ keys.push(key);
353
+ }
354
+ } while (cursor !== "0" && keys.length < input.limit);
355
+ return keys;
356
+ }
357
+ async ensureConnected() {
358
+ if (this.client.isOpen) {
359
+ return;
360
+ }
361
+ try {
362
+ await this.client.connect();
363
+ } catch (error) {
364
+ throw new ExternalServiceError("Failed to connect to Redis.", {
365
+ details: extractErrorMessage(error)
366
+ });
367
+ }
368
+ }
369
+ async execute(args) {
370
+ await this.ensureConnected();
371
+ try {
372
+ return await this.client.sendCommand(args);
373
+ } catch (error) {
374
+ throw new ExternalServiceError(`Redis command failed: ${args[0]}.`, {
375
+ details: extractErrorMessage(error)
376
+ });
377
+ }
378
+ }
379
+ };
380
+ var RedisServer = class extends ToolkitServer {
381
+ constructor(env, client) {
382
+ super(metadata);
383
+ this.env = env;
384
+ this.client = client;
385
+ this.registerTool(
386
+ defineTool({
387
+ name: "get-key",
388
+ title: "Inspect Redis key",
389
+ description: "Fetch a Redis key with type-aware decoding and TTL metadata.",
390
+ inputSchema: {
391
+ key: z.string().min(1),
392
+ sampleSize: z.number().int().positive().max(100).default(10)
393
+ },
394
+ outputSchema: {
395
+ key: z.string(),
396
+ exists: z.boolean(),
397
+ keyType: z.string().nullable(),
398
+ ttlSeconds: z.number().int().nonnegative().nullable(),
399
+ value: jsonValueSchema.nullable(),
400
+ preview: z.string(),
401
+ size: z.number().int().nonnegative().nullable()
402
+ },
403
+ handler: async ({ key, sampleSize }, context) => {
404
+ await context.log("info", `Inspecting Redis key '${key}'.`);
405
+ return this.client.getKey({
406
+ key,
407
+ sampleSize: Math.min(sampleSize, this.env.REDIS_VALUE_SAMPLE_LIMIT)
408
+ });
409
+ },
410
+ renderText: (output) => output.exists ? `${output.key} (${output.keyType ?? "unknown"}) ttl=${output.ttlSeconds ?? "none"}` : `${output.key} was not found.`
411
+ })
412
+ );
413
+ this.registerTool(
414
+ defineTool({
415
+ name: "set-key",
416
+ title: "Set Redis key",
417
+ description: "Set a Redis key with an explicit write guard and optional TTL.",
418
+ inputSchema: {
419
+ key: z.string().min(1),
420
+ value: jsonValueSchema,
421
+ ttlSeconds: z.number().int().positive().max(604800).optional(),
422
+ onlyIfAbsent: z.boolean().default(false),
423
+ allowWrite: z.boolean().default(false)
424
+ },
425
+ outputSchema: {
426
+ key: z.string(),
427
+ stored: z.boolean(),
428
+ ttlSeconds: z.number().int().nonnegative().nullable(),
429
+ serialization: z.string()
430
+ },
431
+ handler: async ({ allowWrite, key, onlyIfAbsent, ttlSeconds, value }, context) => {
432
+ if (!this.env.REDIS_ALLOW_WRITES || !allowWrite) {
433
+ throw new ValidationError(
434
+ "Redis writes are disabled by default. Set REDIS_ALLOW_WRITES=true and pass allowWrite=true to set keys."
435
+ );
436
+ }
437
+ const resolvedTtlSeconds = ttlSeconds ?? (this.env.REDIS_DEFAULT_TTL_SECONDS > 0 ? this.env.REDIS_DEFAULT_TTL_SECONDS : null);
438
+ const serialization = typeof value === "string" ? value : JSON.stringify(value);
439
+ await context.log("warning", `Setting Redis key '${key}' with explicit write opt-in.`);
440
+ const result = await this.client.setKey({
441
+ key,
442
+ serializedValue: serialization,
443
+ ttlSeconds: resolvedTtlSeconds,
444
+ onlyIfAbsent
445
+ });
446
+ return {
447
+ key: result.key,
448
+ stored: result.stored,
449
+ ttlSeconds: result.ttlSeconds,
450
+ serialization
451
+ };
452
+ },
453
+ renderText: (output) => `${output.key} ${output.stored ? "stored" : "not written"}.`
454
+ })
455
+ );
456
+ this.registerTool(
457
+ defineTool({
458
+ name: "inspect-server-info",
459
+ title: "Inspect Redis server info",
460
+ description: "Read INFO output from Redis and parse it into name/value pairs.",
461
+ inputSchema: {
462
+ section: z.string().min(1).optional()
463
+ },
464
+ outputSchema: {
465
+ section: z.string(),
466
+ propertyCount: z.number().int().nonnegative(),
467
+ properties: z.array(
468
+ z.object({
469
+ name: z.string(),
470
+ value: z.string()
471
+ })
472
+ ),
473
+ raw: z.string()
474
+ },
475
+ handler: async ({ section }, context) => {
476
+ await context.log("info", `Inspecting Redis INFO ${section ?? "default"} section.`);
477
+ const request = {};
478
+ if (section) {
479
+ request.section = section;
480
+ }
481
+ const result = await this.client.inspectServerInfo(request);
482
+ return {
483
+ section: result.section,
484
+ propertyCount: result.properties.length,
485
+ properties: result.properties,
486
+ raw: result.raw
487
+ };
488
+ },
489
+ renderText: (output) => `${output.propertyCount} Redis INFO properties returned for ${output.section}.`
490
+ })
491
+ );
492
+ this.registerStaticResource(
493
+ "cache-overview",
494
+ "redis://cache-overview",
495
+ {
496
+ title: "Redis cache overview",
497
+ description: "Redis connection summary, INFO snapshot, and sampled keys.",
498
+ mimeType: "application/json"
499
+ },
500
+ async () => {
501
+ const info = await this.client.inspectServerInfo({ section: "server" });
502
+ const sampleKeys = await this.client.listKeys({
503
+ pattern: this.env.REDIS_RESOURCE_SCAN_PATTERN,
504
+ limit: this.env.REDIS_RESOURCE_KEY_LIMIT
505
+ });
506
+ return this.createJsonResource("redis://cache-overview", {
507
+ connection: maskRedisUrl(this.env.REDIS_URL),
508
+ writeEnabled: this.env.REDIS_ALLOW_WRITES,
509
+ defaultTtlSeconds: this.env.REDIS_DEFAULT_TTL_SECONDS > 0 ? this.env.REDIS_DEFAULT_TTL_SECONDS : null,
510
+ sampledPattern: this.env.REDIS_RESOURCE_SCAN_PATTERN,
511
+ sampleKeys,
512
+ info: info.properties.reduce((accumulator, property) => {
513
+ accumulator[property.name] = property.value;
514
+ return accumulator;
515
+ }, {})
516
+ });
517
+ }
518
+ );
519
+ this.registerPrompt(
520
+ "cache-debug",
521
+ {
522
+ title: "Redis cache debug",
523
+ description: "Generate a focused investigation plan for Redis cache issues.",
524
+ argsSchema: {
525
+ symptom: z.string().min(1),
526
+ keyPattern: z.string().min(1).optional(),
527
+ suspectedTtlIssue: z.boolean().default(false)
528
+ }
529
+ },
530
+ async ({ keyPattern, suspectedTtlIssue, symptom }) => this.createTextPrompt(
531
+ [
532
+ "Investigate the Redis caching issue described below.",
533
+ `Symptom: ${symptom}`,
534
+ `Key pattern: ${keyPattern ?? "not provided"}`,
535
+ `Suspected TTL issue: ${suspectedTtlIssue}`,
536
+ `Write operations enabled: ${this.env.REDIS_ALLOW_WRITES}`,
537
+ "Use the Redis tools to:",
538
+ "- inspect representative keys and their TTLs",
539
+ "- compare INFO output for memory, evictions, and persistence clues",
540
+ "- confirm whether serialization or key type mismatches are involved",
541
+ "- document the safest next step before modifying cache entries"
542
+ ].join("\n")
543
+ )
544
+ );
545
+ }
546
+ async close() {
547
+ await this.client.close?.();
548
+ await super.close();
549
+ }
550
+ };
551
+ function createServer(options = {}) {
552
+ const env = options.env ?? loadEnv(redisEnvShape);
553
+ const client = options.client ?? new NodeRedisClient(env.REDIS_URL);
554
+ return new RedisServer(env, client);
555
+ }
556
+ async function main(argv = process.argv.slice(2)) {
557
+ const env = loadEnv(redisEnvShape);
558
+ const runtimeOptions = parseRuntimeOptions(argv);
559
+ await runToolkitServer(
560
+ {
561
+ createServer: () => createServer({ env }),
562
+ serverCard
563
+ },
564
+ runtimeOptions
565
+ );
566
+ }
567
+ function isMainModule() {
568
+ const entryPoint = process.argv[1];
569
+ if (!entryPoint) {
570
+ return false;
571
+ }
572
+ return import.meta.url === pathToFileURL(entryPoint).href;
573
+ }
574
+ if (isMainModule()) {
575
+ void main().catch((error) => {
576
+ const message = error instanceof Error ? error.message : "Unknown startup error.";
577
+ console.error(`Failed to start Redis MCP server: ${message}`);
578
+ process.exitCode = 1;
579
+ });
580
+ }
581
+ export {
582
+ RedisServer,
583
+ createServer,
584
+ main,
585
+ metadata,
586
+ serverCard
587
+ };
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@universal-mcp-toolkit/server-redis",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Key inspection and cache diagnostics tools for Redis.",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "server-redis": "./dist/index.js",
9
+ "umt-redis": "./dist/index.js"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/Markgatcha/universal-mcp-toolkit.git"
14
+ },
15
+ "homepage": "https://github.com/Markgatcha/universal-mcp-toolkit#readme",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js"
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ ".well-known"
26
+ ],
27
+ "keywords": [
28
+ "mcp",
29
+ "model-context-protocol",
30
+ "ai",
31
+ "developer-tools",
32
+ "typescript",
33
+ "redis",
34
+ "cache",
35
+ "kv-store",
36
+ "diagnostics"
37
+ ],
38
+ "dependencies": {
39
+ "@universal-mcp-toolkit/core": "0.1.0",
40
+ "redis": "^5.11.0",
41
+ "zod": "^4.3.6"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "scripts": {
47
+ "build": "tsup src/index.ts --format esm --dts --clean",
48
+ "dev": "tsx watch src/index.ts",
49
+ "lint": "tsc --noEmit",
50
+ "typecheck": "tsc --noEmit",
51
+ "test": "vitest run --passWithNoTests",
52
+ "clean": "rimraf dist"
53
+ }
54
+ }