@frontmcp/plugins 0.1.2
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 +255 -0
- package/package.json +19 -0
- package/src/cache/cache.plugin.d.ts +12 -0
- package/src/cache/cache.plugin.js +103 -0
- package/src/cache/cache.plugin.js.map +1 -0
- package/src/cache/cache.symbol.d.ts +1 -0
- package/src/cache/cache.symbol.js +5 -0
- package/src/cache/cache.symbol.js.map +1 -0
- package/src/cache/cache.types.d.ts +45 -0
- package/src/cache/cache.types.js +3 -0
- package/src/cache/cache.types.js.map +1 -0
- package/src/cache/index.d.ts +2 -0
- package/src/cache/index.js +8 -0
- package/src/cache/index.js.map +1 -0
- package/src/cache/providers/cache-memory.provider.d.ts +19 -0
- package/src/cache/providers/cache-memory.provider.js +109 -0
- package/src/cache/providers/cache-memory.provider.js.map +1 -0
- package/src/cache/providers/cache-redis.provider.d.ts +15 -0
- package/src/cache/providers/cache-redis.provider.js +68 -0
- package/src/cache/providers/cache-redis.provider.js.map +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# MCP Gateway — Plugins
|
|
2
|
+
|
|
3
|
+
Pluggable extensions for MCP Gateway live here. Each plugin can contribute **providers**, **hooks**, and optional **adapters** that extend the platform.
|
|
4
|
+
|
|
5
|
+
If you want to use a specific plugin, open that plugin’s README for full details. This page serves as an index and a contributor guide.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Table of contents
|
|
10
|
+
|
|
11
|
+
- [Available plugins](#available-plugins)
|
|
12
|
+
- [Quick start: enabling a plugin](#quick-start-enabling-a-plugin)
|
|
13
|
+
- [Contributor guide: authoring a plugin](#contributor-guide-authoring-a-plugin)
|
|
14
|
+
- [1) Recommended folder layout](#1-recommended-folder-layout)
|
|
15
|
+
- [2) Export surface (`index.ts`)](#2-export-surface-indexts)
|
|
16
|
+
- [3) Type augmentation for `@McpTool`](#3-type-augmentation-for-mcptool)
|
|
17
|
+
- [4) Implementing the plugin class](#4-implementing-the-plugin-class)
|
|
18
|
+
- [5) Initialization styles (`DynamicPlugin.init`)](#5-initialization-styles-dynamicplugininit)
|
|
19
|
+
- [6) Hooks contributed by plugins](#6-hooks-contributed-by-plugins)
|
|
20
|
+
- [7) Registering your plugin in an app](#7-registering-your-plugin-in-an-app)
|
|
21
|
+
- [8) Documentation checklist](#8-documentation-checklist)
|
|
22
|
+
- [9) Forward-looking hook families](#9-forward-looking-hook-families)
|
|
23
|
+
- [Contributing](#contributing)
|
|
24
|
+
- [License](#license)
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Available plugins
|
|
29
|
+
|
|
30
|
+
| Plugin | Description | Docs | Path |
|
|
31
|
+
|-------|-------------|------|------|
|
|
32
|
+
| Cache | Transparent caching for tool outputs keyed by input. Supports in-memory and Redis stores; per-tool TTL and sliding windows. | [Cache Plugin README](./src/cache/README.md) | [`src/cache`](./src/cache) |
|
|
33
|
+
|
|
34
|
+
> For configuration and usage examples, follow the plugin’s own README.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Quick start: enabling a plugin
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
// app.ts
|
|
42
|
+
import { McpApp } from '@frontmcp/sdk';
|
|
43
|
+
import CachePlugin from '@frontmcp/plugins/cache';
|
|
44
|
+
|
|
45
|
+
@McpApp({
|
|
46
|
+
name: 'my-app',
|
|
47
|
+
plugins: [
|
|
48
|
+
CachePlugin, // or CachePlugin.init({...}) — see below for init styles
|
|
49
|
+
],
|
|
50
|
+
})
|
|
51
|
+
export default class MyApp {}
|
|
52
|
+
````
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Contributor guide: authoring a plugin
|
|
57
|
+
|
|
58
|
+
The sections below summarize structure, type augmentation, dynamic providers, and hooks.
|
|
59
|
+
|
|
60
|
+
### 1) Recommended folder layout
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
plugins/
|
|
64
|
+
src/<your-plugin>/
|
|
65
|
+
├─ <your-plugin>.plugin.ts
|
|
66
|
+
├─ <your-plugin>.types.ts
|
|
67
|
+
├─ <your-plugin>.symbol.ts # optional, for DI tokens
|
|
68
|
+
├─ providers/ # optional, runtime providers
|
|
69
|
+
│ └─ ...
|
|
70
|
+
├─ index.ts
|
|
71
|
+
└─ README.md # user-facing docs
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
> If your repo uses a monorepo layout like `libs/plugins/src/...`, keep the same structure under that root. Paths in this README are relative to the current `plugins` folder.
|
|
75
|
+
|
|
76
|
+
### 2) Export surface (`index.ts`)
|
|
77
|
+
|
|
78
|
+
At `src/<your-plugin>/index.ts`, re-export the plugin and its types:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
export { default } from './<your-plugin>.plugin';
|
|
82
|
+
export * from './<your-plugin>.types';
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 3) Type augmentation for `@McpTool`
|
|
86
|
+
|
|
87
|
+
If your plugin adds tool-level options (e.g., `@McpTool({ myFeature: {...} })`), augment the ambient `ToolMetadata` interface so tool authors get type-safe options.
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
// src/my-feature/my-feature.types.ts
|
|
91
|
+
|
|
92
|
+
declare global {
|
|
93
|
+
interface ToolMetadata {
|
|
94
|
+
/** Enables MyFeature for a tool; `true` uses plugin defaults. */
|
|
95
|
+
myFeature?: MyFeatureToolOptions | true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface MyFeatureToolOptions {
|
|
100
|
+
level?: 'low' | 'medium' | 'high';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface MyFeaturePluginOptions {
|
|
104
|
+
/** Options provided at plugin registration time. */
|
|
105
|
+
defaultLevel?: 'low' | 'medium' | 'high';
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
> Why: `declare global` merges into the ambient `ToolMetadata` used by `@McpTool`, so TypeScript validates options wherever tools are defined.
|
|
110
|
+
|
|
111
|
+
### 4) Implementing the plugin class
|
|
112
|
+
|
|
113
|
+
Plugins are classes decorated with `@McpPlugin(...)`. For plugins that need configuration and/or generated providers, extend `DynamicPlugin<TOptions>` so you can support both value and factory initialization while contributing dynamic providers.
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
// src/my-feature/my-feature.plugin.ts
|
|
117
|
+
import {
|
|
118
|
+
McpPlugin,
|
|
119
|
+
DynamicPlugin,
|
|
120
|
+
ToolHook,
|
|
121
|
+
ToolHookStage,
|
|
122
|
+
McpProviderType,
|
|
123
|
+
} from '@frontmcp/sdk';
|
|
124
|
+
import { MyFeaturePluginOptions } from './my-feature.types';
|
|
125
|
+
|
|
126
|
+
@McpPlugin({
|
|
127
|
+
name: 'plugin:my-feature',
|
|
128
|
+
description: 'Does something useful',
|
|
129
|
+
providers: [
|
|
130
|
+
// Static providers that always load with the plugin (optional)
|
|
131
|
+
// { provide: MyToken, useClass: MyProvider },
|
|
132
|
+
],
|
|
133
|
+
})
|
|
134
|
+
export default class MyFeaturePlugin extends DynamicPlugin<MyFeaturePluginOptions> {
|
|
135
|
+
static defaultOptions: MyFeaturePluginOptions = {
|
|
136
|
+
defaultLevel: 'medium',
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Contribute providers based on resolved options (runs before instance creation)
|
|
140
|
+
static override dynamicProviders(options: MyFeaturePluginOptions): readonly McpProviderType[] {
|
|
141
|
+
const providers: McpProviderType[] = [];
|
|
142
|
+
// Decide implementations based on options
|
|
143
|
+
// providers.push({ provide: MyToken, useValue: new MyProvider(options) });
|
|
144
|
+
return providers;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
constructor(public readonly options: MyFeaturePluginOptions = MyFeaturePlugin.defaultOptions) {
|
|
148
|
+
super();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Optional: register global tool hooks contributed by the plugin
|
|
152
|
+
@ToolHook(ToolHookStage.willExecute)
|
|
153
|
+
async willExecute(ctx: any) {
|
|
154
|
+
// Observe/mutate ctx.input before tool execution
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### 5) Initialization styles (`DynamicPlugin.init`)
|
|
160
|
+
|
|
161
|
+
`DynamicPlugin` exposes a static `init()` so apps can register your plugin in different ways:
|
|
162
|
+
|
|
163
|
+
* **Raw class** — zero-arg constructor only; no dynamic providers from options:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
plugins: [MyFeaturePlugin]
|
|
167
|
+
```
|
|
168
|
+
* **Value style** — options known upfront; `dynamicProviders(options)` is evaluated and merged:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
plugins: [MyFeaturePlugin.init({ defaultLevel: 'high' })]
|
|
172
|
+
```
|
|
173
|
+
* **Factory style** — compute options from app DI; then merge `dynamicProviders(realOptions)`:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
plugins: [
|
|
177
|
+
MyFeaturePlugin.init({
|
|
178
|
+
inject: () => [SomeConfig],
|
|
179
|
+
useFactory: (cfg) => ({ defaultLevel: cfg.level }),
|
|
180
|
+
}),
|
|
181
|
+
]
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Under the hood (high level):
|
|
185
|
+
|
|
186
|
+
* Static providers from `@McpPlugin({ providers: [...] })` are merged first.
|
|
187
|
+
* In **value**/**factory** styles, the registry evaluates `dynamicProviders(...)` and merges results.
|
|
188
|
+
* Provider tokens are de-duplicated to avoid conflicts.
|
|
189
|
+
|
|
190
|
+
> Implementation references (repository paths may vary):
|
|
191
|
+
> `../common/src/plugins/dynamic.plugin.ts` and `../core/src/plugin/plugin.registry.ts`.
|
|
192
|
+
|
|
193
|
+
### 6) Hooks contributed by plugins
|
|
194
|
+
|
|
195
|
+
Plugins can register global tool hooks via `@ToolHook(stage)`. Hooks run for every tool unless filtered by metadata. Common stages include (examples; depends on platform version):
|
|
196
|
+
|
|
197
|
+
* `willReadCache`, `willWriteCache` (see Cache plugin)
|
|
198
|
+
* `willParseInput`, `willValidateInput`, `willExecute`, `didExecute`, `willFinalizeInvoke`
|
|
199
|
+
|
|
200
|
+
See the cache example hooks in: [`src/cache/cache.plugin.ts`](./src/cache/cache.plugin.ts).
|
|
201
|
+
|
|
202
|
+
### 7) Registering your plugin in an app
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
import { McpApp } from '@frontmcp/sdk';
|
|
206
|
+
import MyFeaturePlugin from '@frontmcp/plugins/my-feature';
|
|
207
|
+
|
|
208
|
+
@McpApp({
|
|
209
|
+
name: 'my-app',
|
|
210
|
+
plugins: [
|
|
211
|
+
MyFeaturePlugin, // or MyFeaturePlugin.init({...})
|
|
212
|
+
],
|
|
213
|
+
})
|
|
214
|
+
export default class MyApp {}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### 8) Documentation checklist
|
|
218
|
+
|
|
219
|
+
Each plugin must ship a `README.md` that explains:
|
|
220
|
+
|
|
221
|
+
* **What it does** and **when to use it**
|
|
222
|
+
* **Installation & registration** examples (raw/value/factory)
|
|
223
|
+
* **Configuration options** (plugin-level and tool-level)
|
|
224
|
+
* **Providers** it contributes and any required external services
|
|
225
|
+
* **Hooks** it adds and how they affect tool/app behavior
|
|
226
|
+
* **Examples** (minimal and advanced)
|
|
227
|
+
|
|
228
|
+
For a concrete example, see the [Cache Plugin README](./src/cache/README.md).
|
|
229
|
+
|
|
230
|
+
### 9) Forward-looking hook families
|
|
231
|
+
|
|
232
|
+
These hook families are planned. Design plugins with extension points in mind:
|
|
233
|
+
|
|
234
|
+
* **AppHook** — app bootstrap/shutdown lifecycle.
|
|
235
|
+
* **HttpHook** — inbound/outbound request/response (tracing, auditing, rate limiting, etc.).
|
|
236
|
+
* **AuthHook** — authentication/authorization lifecycle (challenge, success, failure).
|
|
237
|
+
* **AdapterHooks** — participate in adapter initialization/configuration.
|
|
238
|
+
* **IO Hooks** — monitor and optionally block filesystem/native calls (policy/sandbox).
|
|
239
|
+
|
|
240
|
+
As these land, plugin authors will be able to add `@AppHook`, `@HttpHook`, `@AuthHook`, etc., similarly to `@ToolHook`.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Contributing
|
|
245
|
+
|
|
246
|
+
1. Create your plugin under `src/<your-plugin>/` following the layout above.
|
|
247
|
+
2. Include a thorough `README.md` in your plugin folder.
|
|
248
|
+
3. Add your plugin to the **Available plugins** table (name, short description, links).
|
|
249
|
+
4. Submit a PR with tests and lint passing.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## License
|
|
254
|
+
|
|
255
|
+
This folder inherits the repository’s license unless otherwise noted in individual plugin folders.
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@frontmcp/plugins",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"type": "commonjs",
|
|
5
|
+
"main": "./dist/src/index.js",
|
|
6
|
+
"types": "./dist/src/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
"./package.json": "./package.json",
|
|
9
|
+
"./cache": {
|
|
10
|
+
"development": "./src/cache/index.ts",
|
|
11
|
+
"types": "./dist/src/cache/index.d.ts",
|
|
12
|
+
"import": "./dist/src/cache/index.js",
|
|
13
|
+
"default": "./dist/src/cache/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"tslib": "^2.3.0"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { DynamicPlugin, ProviderType } from '@frontmcp/sdk';
|
|
2
|
+
import { ToolInvokeContext } from '@frontmcp/core';
|
|
3
|
+
import { CachePluginOptions } from './cache.types';
|
|
4
|
+
export default class CachePlugin extends DynamicPlugin<CachePluginOptions> {
|
|
5
|
+
private readonly defaultTTL;
|
|
6
|
+
static dynamicProviders: (options: CachePluginOptions) => ProviderType[];
|
|
7
|
+
static defaultOptions: CachePluginOptions;
|
|
8
|
+
options: CachePluginOptions;
|
|
9
|
+
constructor(options?: CachePluginOptions);
|
|
10
|
+
willReadCache(ctx: ToolInvokeContext): Promise<void>;
|
|
11
|
+
willWriteCache(ctx: ToolInvokeContext): Promise<void>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var CachePlugin_1;
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const sdk_1 = require("@frontmcp/sdk");
|
|
6
|
+
const core_1 = require("@frontmcp/core");
|
|
7
|
+
const file_hasher_1 = require("nx/src/hasher/file-hasher");
|
|
8
|
+
const cache_redis_provider_1 = tslib_1.__importDefault(require("./providers/cache-redis.provider"));
|
|
9
|
+
const cache_memory_provider_1 = tslib_1.__importDefault(require("./providers/cache-memory.provider"));
|
|
10
|
+
const cache_symbol_1 = require("./cache.symbol");
|
|
11
|
+
let CachePlugin = CachePlugin_1 = class CachePlugin extends sdk_1.DynamicPlugin {
|
|
12
|
+
constructor(options = CachePlugin_1.defaultOptions) {
|
|
13
|
+
super();
|
|
14
|
+
this.options = {
|
|
15
|
+
defaultTTL: 60 * 60 * 24,
|
|
16
|
+
...options,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
async willReadCache(ctx) {
|
|
20
|
+
const { cache } = ctx.metadata;
|
|
21
|
+
if (!cache) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const redis = ctx.get(cache_symbol_1.CacheStoreToken);
|
|
25
|
+
const hash = (0, file_hasher_1.hashObject)(ctx.input);
|
|
26
|
+
const cached = await redis.getValue(hash);
|
|
27
|
+
if (cache == true || (cache.ttl && cache.slideWindow)) {
|
|
28
|
+
const ttl = cache === true ? this.defaultTTL : cache.ttl ?? this.defaultTTL;
|
|
29
|
+
await redis.setValue(hash, cached, ttl);
|
|
30
|
+
}
|
|
31
|
+
if (cached) {
|
|
32
|
+
ctx.respond({
|
|
33
|
+
...cached,
|
|
34
|
+
___cached__: true,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async willWriteCache(ctx) {
|
|
39
|
+
const { cache } = ctx.metadata;
|
|
40
|
+
if (!cache) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
console.log('willWriteCache', { cache });
|
|
44
|
+
const ttl = cache === true ? this.defaultTTL : cache.ttl ?? this.defaultTTL;
|
|
45
|
+
const redis = ctx.get(cache_symbol_1.CacheStoreToken);
|
|
46
|
+
const hash = (0, file_hasher_1.hashObject)(ctx.input);
|
|
47
|
+
await redis.setValue(hash, ctx.output, ttl);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
CachePlugin.dynamicProviders = (options) => {
|
|
51
|
+
const providers = [];
|
|
52
|
+
switch (options.type) {
|
|
53
|
+
case 'redis':
|
|
54
|
+
case 'redis-client':
|
|
55
|
+
providers.push({
|
|
56
|
+
name: 'cache:redis',
|
|
57
|
+
provide: cache_symbol_1.CacheStoreToken,
|
|
58
|
+
useValue: new cache_redis_provider_1.default(options),
|
|
59
|
+
});
|
|
60
|
+
break;
|
|
61
|
+
case 'memory':
|
|
62
|
+
providers.push({
|
|
63
|
+
name: 'cache:memory',
|
|
64
|
+
provide: cache_symbol_1.CacheStoreToken,
|
|
65
|
+
useValue: new cache_memory_provider_1.default(options.defaultTTL),
|
|
66
|
+
});
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
return providers;
|
|
70
|
+
};
|
|
71
|
+
CachePlugin.defaultOptions = {
|
|
72
|
+
type: 'memory',
|
|
73
|
+
};
|
|
74
|
+
tslib_1.__decorate([
|
|
75
|
+
(0, sdk_1.ToolHook)(sdk_1.ToolHookStage.willReadCache),
|
|
76
|
+
tslib_1.__metadata("design:type", Function),
|
|
77
|
+
tslib_1.__metadata("design:paramtypes", [core_1.ToolInvokeContext]),
|
|
78
|
+
tslib_1.__metadata("design:returntype", Promise)
|
|
79
|
+
], CachePlugin.prototype, "willReadCache", null);
|
|
80
|
+
tslib_1.__decorate([
|
|
81
|
+
(0, sdk_1.ToolHook)(sdk_1.ToolHookStage.willWriteCache),
|
|
82
|
+
tslib_1.__metadata("design:type", Function),
|
|
83
|
+
tslib_1.__metadata("design:paramtypes", [core_1.ToolInvokeContext]),
|
|
84
|
+
tslib_1.__metadata("design:returntype", Promise)
|
|
85
|
+
], CachePlugin.prototype, "willWriteCache", null);
|
|
86
|
+
CachePlugin = CachePlugin_1 = tslib_1.__decorate([
|
|
87
|
+
(0, sdk_1.Plugin)({
|
|
88
|
+
name: 'cache',
|
|
89
|
+
description: 'Cache plugin for caching tool results',
|
|
90
|
+
providers: [
|
|
91
|
+
/* add providers that always loaded with the plugin or default providers */
|
|
92
|
+
{
|
|
93
|
+
// this is default provider for cache, will be overridden if dynamicProviders based on config
|
|
94
|
+
name: 'cache:memory',
|
|
95
|
+
provide: cache_symbol_1.CacheStoreToken,
|
|
96
|
+
useValue: new cache_memory_provider_1.default(60 * 60 * 24),
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
}),
|
|
100
|
+
tslib_1.__metadata("design:paramtypes", [Object])
|
|
101
|
+
], CachePlugin);
|
|
102
|
+
exports.default = CachePlugin;
|
|
103
|
+
//# sourceMappingURL=cache.plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.plugin.js","sourceRoot":"","sources":["../../../src/cache/cache.plugin.ts"],"names":[],"mappings":"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","sourcesContent":["import { DynamicPlugin, Plugin, ToolHook, ProviderType, ToolHookStage } from '@frontmcp/sdk';\nimport { ToolInvokeContext } from '@frontmcp/core';\nimport { hashObject } from 'nx/src/hasher/file-hasher';\nimport CacheRedisProvider from './providers/cache-redis.provider';\nimport CacheMemoryProvider from './providers/cache-memory.provider';\nimport { CachePluginOptions, CacheStoreInterface } from './cache.types';\nimport { CacheStoreToken } from './cache.symbol';\n\n@Plugin({\n name: 'cache',\n description: 'Cache plugin for caching tool results',\n providers: [\n /* add providers that always loaded with the plugin or default providers */\n {\n // this is default provider for cache, will be overridden if dynamicProviders based on config\n name: 'cache:memory',\n provide: CacheStoreToken,\n useValue: new CacheMemoryProvider(60 * 60 * 24),\n },\n ],\n})\nexport default class CachePlugin extends DynamicPlugin<CachePluginOptions> {\n private readonly defaultTTL: number;\n\n static override dynamicProviders = (options: CachePluginOptions) => {\n const providers: ProviderType[] = [];\n switch (options.type) {\n case 'redis':\n case 'redis-client':\n providers.push({\n name: 'cache:redis',\n provide: CacheStoreToken,\n useValue: new CacheRedisProvider(options),\n });\n break;\n case 'memory':\n providers.push({\n name: 'cache:memory',\n provide: CacheStoreToken,\n useValue: new CacheMemoryProvider(options.defaultTTL),\n });\n break;\n }\n return providers;\n };\n\n static defaultOptions: CachePluginOptions = {\n type: 'memory',\n };\n options: CachePluginOptions;\n\n constructor(options: CachePluginOptions = CachePlugin.defaultOptions) {\n super();\n this.options = {\n defaultTTL: 60 * 60 * 24,\n ...options,\n };\n }\n\n @ToolHook(ToolHookStage.willReadCache)\n async willReadCache(ctx: ToolInvokeContext) {\n const { cache } = ctx.metadata;\n if (!cache) {\n return;\n }\n\n const redis = ctx.get<CacheStoreInterface>(CacheStoreToken);\n const hash = hashObject(ctx.input);\n const cached = await redis.getValue(hash);\n\n if (cache == true || (cache.ttl && cache.slideWindow)) {\n const ttl = cache === true ? this.defaultTTL : cache.ttl ?? this.defaultTTL;\n await redis.setValue(hash, cached, ttl);\n }\n\n if (cached) {\n ctx.respond({\n ...cached,\n ___cached__: true,\n });\n }\n }\n\n @ToolHook(ToolHookStage.willWriteCache)\n async willWriteCache(ctx: ToolInvokeContext) {\n const { cache } = ctx.metadata;\n if (!cache) {\n return;\n }\n console.log('willWriteCache', { cache });\n const ttl = cache === true ? this.defaultTTL : cache.ttl ?? this.defaultTTL;\n\n const redis = ctx.get<CacheStoreInterface>(CacheStoreToken);\n const hash = hashObject(ctx.input);\n await redis.setValue(hash, ctx.output, ttl);\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const CacheStoreToken: unique symbol;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.symbol.js","sourceRoot":"","sources":["../../../src/cache/cache.symbol.ts"],"names":[],"mappings":";;;AAAa,QAAA,eAAe,GAAG,MAAM,CAAC,oBAAoB,CAAC,CAAC","sourcesContent":["export const CacheStoreToken = Symbol('plugin:cache:store');\n"]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Redis as RedisClient } from 'ioredis';
|
|
2
|
+
declare global {
|
|
3
|
+
interface ExtendFrontMcpToolMetadata {
|
|
4
|
+
cache?: CachePluginToolOptions | true;
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export interface CachePluginToolOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Time to live in seconds. Default is 1 day.
|
|
10
|
+
*/
|
|
11
|
+
ttl?: number;
|
|
12
|
+
/**
|
|
13
|
+
* If true, the cache value will be updated with the new value after the TTL.
|
|
14
|
+
* Default is false.
|
|
15
|
+
*/
|
|
16
|
+
slideWindow?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface BaseCachePluginOptions {
|
|
19
|
+
defaultTTL?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface RedisClientCachePluginOptions extends BaseCachePluginOptions {
|
|
22
|
+
type: 'redis-client';
|
|
23
|
+
client: RedisClient;
|
|
24
|
+
}
|
|
25
|
+
export interface RedisCachePluginOptions extends BaseCachePluginOptions {
|
|
26
|
+
type: 'redis';
|
|
27
|
+
config: {
|
|
28
|
+
host: string;
|
|
29
|
+
port: number;
|
|
30
|
+
password?: string;
|
|
31
|
+
db?: number;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export type MemoryCachePluginOptions = BaseCachePluginOptions & {
|
|
35
|
+
type: 'memory';
|
|
36
|
+
};
|
|
37
|
+
export type RedisCacheOptions = RedisClientCachePluginOptions | RedisCachePluginOptions;
|
|
38
|
+
export type CachePluginOptions = MemoryCachePluginOptions | RedisCacheOptions;
|
|
39
|
+
export interface CacheStoreInterface {
|
|
40
|
+
setValue(key: string, value: any, ttlSeconds?: number): Promise<void>;
|
|
41
|
+
getValue<T = any>(key: string, defaultValue?: T): Promise<T | undefined>;
|
|
42
|
+
delete(key: string): Promise<void>;
|
|
43
|
+
exists(key: string): Promise<boolean>;
|
|
44
|
+
close(): Promise<void>;
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.types.js","sourceRoot":"","sources":["../../../src/cache/cache.types.ts"],"names":[],"mappings":"","sourcesContent":["import { Redis as RedisClient } from 'ioredis';\ndeclare global {\n interface ExtendFrontMcpToolMetadata {\n cache?: CachePluginToolOptions | true;\n }\n}\n\nexport interface CachePluginToolOptions {\n /**\n * Time to live in seconds. Default is 1 day.\n */\n ttl?: number; // default 1 day\n\n /**\n * If true, the cache value will be updated with the new value after the TTL.\n * Default is false.\n */\n slideWindow?: boolean;\n}\n\nexport interface BaseCachePluginOptions {\n defaultTTL?: number; // default 1 day\n}\n\nexport interface RedisClientCachePluginOptions extends BaseCachePluginOptions {\n type: 'redis-client';\n client: RedisClient;\n}\nexport interface RedisCachePluginOptions extends BaseCachePluginOptions {\n type: 'redis';\n config: {\n host: string;\n port: number;\n password?: string;\n db?: number;\n };\n}\nexport type MemoryCachePluginOptions = BaseCachePluginOptions & {\n type: 'memory';\n};\n\nexport type RedisCacheOptions = RedisClientCachePluginOptions | RedisCachePluginOptions;\n\nexport type CachePluginOptions = MemoryCachePluginOptions | RedisCacheOptions;\n\nexport interface CacheStoreInterface {\n setValue(key: string, value: any, ttlSeconds?: number): Promise<void>;\n getValue<T = any>(key: string, defaultValue?: T): Promise<T | undefined>;\n delete(key: string): Promise<void>;\n exists(key: string): Promise<boolean>;\n close(): Promise<void>;\n}\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
var cache_plugin_1 = require("./cache.plugin");
|
|
6
|
+
Object.defineProperty(exports, "default", { enumerable: true, get: function () { return tslib_1.__importDefault(cache_plugin_1).default; } });
|
|
7
|
+
tslib_1.__exportStar(require("./cache.types"), exports);
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/cache/index.ts"],"names":[],"mappings":";;;;AAAA,+CAAyC;AAAhC,gIAAA,OAAO,OAAA;AAChB,wDAA8B","sourcesContent":["export { default } from './cache.plugin';\nexport * from './cache.types';\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { CacheStoreInterface } from '../cache.types';
|
|
2
|
+
export default class CacheMemoryProvider implements CacheStoreInterface {
|
|
3
|
+
private readonly memory;
|
|
4
|
+
private sweeper?;
|
|
5
|
+
constructor(sweepIntervalTTL?: number);
|
|
6
|
+
/** Set any value (auto-stringifies objects) */
|
|
7
|
+
setValue(key: string, value: any, ttlSeconds?: number): Promise<void>;
|
|
8
|
+
/** Get a value and automatically parse JSON if possible */
|
|
9
|
+
getValue<T = any>(key: string, defaultValue?: T): Promise<T | undefined>;
|
|
10
|
+
/** Delete a key */
|
|
11
|
+
delete(key: string): Promise<void>;
|
|
12
|
+
/** Check if a key exists (and not expired) */
|
|
13
|
+
exists(key: string): Promise<boolean>;
|
|
14
|
+
/** Gracefully close the provider */
|
|
15
|
+
close(): Promise<void>;
|
|
16
|
+
private isExpired;
|
|
17
|
+
/** Periodically remove expired keys to keep memory tidy */
|
|
18
|
+
private sweep;
|
|
19
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const sdk_1 = require("@frontmcp/sdk");
|
|
5
|
+
const MAX_TIMEOUT_MS = 2 ** 31 - 1; // ~24.8 days (Node setTimeout limit)
|
|
6
|
+
let CacheMemoryProvider = class CacheMemoryProvider {
|
|
7
|
+
constructor(sweepIntervalTTL = 60) {
|
|
8
|
+
this.memory = new Map();
|
|
9
|
+
this.sweeper = setInterval(() => this.sweep(), sweepIntervalTTL * 1000);
|
|
10
|
+
// don’t keep the process alive just for the sweeper (Node >=14)
|
|
11
|
+
this.sweeper.unref?.();
|
|
12
|
+
}
|
|
13
|
+
/** Set any value (auto-stringifies objects) */
|
|
14
|
+
async setValue(key, value, ttlSeconds) {
|
|
15
|
+
const strValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
16
|
+
// clear any previous timeout on this key
|
|
17
|
+
const existing = this.memory.get(key);
|
|
18
|
+
if (existing?.timeout)
|
|
19
|
+
clearTimeout(existing.timeout);
|
|
20
|
+
const entry = { value: strValue };
|
|
21
|
+
if (ttlSeconds && ttlSeconds > 0) {
|
|
22
|
+
const ttlMs = ttlSeconds * 1000;
|
|
23
|
+
entry.expiresAt = Date.now() + ttlMs;
|
|
24
|
+
// Only schedule a timer if within Node's setTimeout limit; otherwise rely on sweeper/lazy purge
|
|
25
|
+
if (ttlMs <= MAX_TIMEOUT_MS) {
|
|
26
|
+
entry.timeout = setTimeout(() => {
|
|
27
|
+
// final check guards against clock drift or updates
|
|
28
|
+
const e = this.memory.get(key);
|
|
29
|
+
if (e && e.expiresAt && e.expiresAt <= Date.now()) {
|
|
30
|
+
this.memory.delete(key);
|
|
31
|
+
}
|
|
32
|
+
}, ttlMs);
|
|
33
|
+
entry.timeout.unref?.();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
this.memory.set(key, entry);
|
|
37
|
+
}
|
|
38
|
+
/** Get a value and automatically parse JSON if possible */
|
|
39
|
+
async getValue(key, defaultValue) {
|
|
40
|
+
const entry = this.memory.get(key);
|
|
41
|
+
if (!entry)
|
|
42
|
+
return defaultValue;
|
|
43
|
+
if (this.isExpired(entry)) {
|
|
44
|
+
await this.delete(key);
|
|
45
|
+
return defaultValue;
|
|
46
|
+
}
|
|
47
|
+
const raw = entry.value;
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(raw);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// fallback for plain string values
|
|
53
|
+
return raw;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Delete a key */
|
|
57
|
+
async delete(key) {
|
|
58
|
+
const entry = this.memory.get(key);
|
|
59
|
+
if (entry?.timeout)
|
|
60
|
+
clearTimeout(entry.timeout);
|
|
61
|
+
this.memory.delete(key);
|
|
62
|
+
}
|
|
63
|
+
/** Check if a key exists (and not expired) */
|
|
64
|
+
async exists(key) {
|
|
65
|
+
const entry = this.memory.get(key);
|
|
66
|
+
if (!entry)
|
|
67
|
+
return false;
|
|
68
|
+
if (this.isExpired(entry)) {
|
|
69
|
+
await this.delete(key);
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
/** Gracefully close the provider */
|
|
75
|
+
async close() {
|
|
76
|
+
if (this.sweeper)
|
|
77
|
+
clearInterval(this.sweeper);
|
|
78
|
+
for (const [, entry] of this.memory) {
|
|
79
|
+
if (entry.timeout)
|
|
80
|
+
clearTimeout(entry.timeout);
|
|
81
|
+
}
|
|
82
|
+
this.memory.clear();
|
|
83
|
+
}
|
|
84
|
+
// ---- internals ----
|
|
85
|
+
isExpired(entry) {
|
|
86
|
+
return entry.expiresAt !== undefined && entry.expiresAt <= Date.now();
|
|
87
|
+
}
|
|
88
|
+
/** Periodically remove expired keys to keep memory tidy */
|
|
89
|
+
sweep() {
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
for (const [key, entry] of this.memory) {
|
|
92
|
+
if (entry.expiresAt !== undefined && entry.expiresAt <= now) {
|
|
93
|
+
if (entry.timeout)
|
|
94
|
+
clearTimeout(entry.timeout);
|
|
95
|
+
this.memory.delete(key);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
CacheMemoryProvider = tslib_1.__decorate([
|
|
101
|
+
(0, sdk_1.Provider)({
|
|
102
|
+
name: 'provider:cache:memory',
|
|
103
|
+
description: 'Memory-based cache provider',
|
|
104
|
+
scope: sdk_1.ProviderScope.GLOBAL,
|
|
105
|
+
}),
|
|
106
|
+
tslib_1.__metadata("design:paramtypes", [Object])
|
|
107
|
+
], CacheMemoryProvider);
|
|
108
|
+
exports.default = CacheMemoryProvider;
|
|
109
|
+
//# sourceMappingURL=cache-memory.provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-memory.provider.js","sourceRoot":"","sources":["../../../../src/cache/providers/cache-memory.provider.ts"],"names":[],"mappings":"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","sourcesContent":["import { Provider, ProviderScope } from '@frontmcp/sdk';\nimport { CacheStoreInterface } from '../cache.types';\n\ntype Entry = {\n value: string;\n /** epoch millis when this entry expires (undefined = no TTL) */\n expiresAt?: number;\n /** per-key timeout when TTL is short enough to schedule */\n timeout?: NodeJS.Timeout;\n};\n\nconst MAX_TIMEOUT_MS = 2 ** 31 - 1; // ~24.8 days (Node setTimeout limit)\n\n@Provider({\n name: 'provider:cache:memory',\n description: 'Memory-based cache provider',\n scope: ProviderScope.GLOBAL,\n})\nexport default class CacheMemoryProvider implements CacheStoreInterface{\n private readonly memory = new Map<string, Entry>();\n private sweeper?: NodeJS.Timeout;\n\n constructor(sweepIntervalTTL = 60) {\n this.sweeper = setInterval(() => this.sweep(), sweepIntervalTTL * 1000);\n // don’t keep the process alive just for the sweeper (Node >=14)\n (this.sweeper as any).unref?.();\n }\n\n /** Set any value (auto-stringifies objects) */\n async setValue(key: string, value: any, ttlSeconds?: number): Promise<void> {\n const strValue = typeof value === 'string' ? value : JSON.stringify(value);\n\n // clear any previous timeout on this key\n const existing = this.memory.get(key);\n if (existing?.timeout) clearTimeout(existing.timeout);\n\n const entry: Entry = { value: strValue };\n\n if (ttlSeconds && ttlSeconds > 0) {\n const ttlMs = ttlSeconds * 1000;\n entry.expiresAt = Date.now() + ttlMs;\n\n // Only schedule a timer if within Node's setTimeout limit; otherwise rely on sweeper/lazy purge\n if (ttlMs <= MAX_TIMEOUT_MS) {\n entry.timeout = setTimeout(() => {\n // final check guards against clock drift or updates\n const e = this.memory.get(key);\n if (e && e.expiresAt && e.expiresAt <= Date.now()) {\n this.memory.delete(key);\n }\n }, ttlMs);\n (entry.timeout as any).unref?.();\n }\n }\n\n this.memory.set(key, entry);\n }\n\n /** Get a value and automatically parse JSON if possible */\n async getValue<T = any>(key: string, defaultValue?: T): Promise<T | undefined> {\n const entry = this.memory.get(key);\n if (!entry) return defaultValue;\n\n if (this.isExpired(entry)) {\n await this.delete(key);\n return defaultValue;\n }\n\n const raw = entry.value;\n try {\n return JSON.parse(raw) as T;\n } catch {\n // fallback for plain string values\n return raw as unknown as T;\n }\n }\n\n /** Delete a key */\n async delete(key: string): Promise<void> {\n const entry = this.memory.get(key);\n if (entry?.timeout) clearTimeout(entry.timeout);\n this.memory.delete(key);\n }\n\n /** Check if a key exists (and not expired) */\n async exists(key: string): Promise<boolean> {\n const entry = this.memory.get(key);\n if (!entry) return false;\n if (this.isExpired(entry)) {\n await this.delete(key);\n return false;\n }\n return true;\n }\n\n /** Gracefully close the provider */\n async close(): Promise<void> {\n if (this.sweeper) clearInterval(this.sweeper);\n for (const [, entry] of this.memory) {\n if (entry.timeout) clearTimeout(entry.timeout);\n }\n this.memory.clear();\n }\n\n // ---- internals ----\n\n private isExpired(entry: Entry): boolean {\n return entry.expiresAt !== undefined && entry.expiresAt <= Date.now();\n }\n\n /** Periodically remove expired keys to keep memory tidy */\n private sweep(): void {\n const now = Date.now();\n for (const [key, entry] of this.memory) {\n if (entry.expiresAt !== undefined && entry.expiresAt <= now) {\n if (entry.timeout) clearTimeout(entry.timeout);\n this.memory.delete(key);\n }\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CacheStoreInterface, RedisCacheOptions } from '../cache.types';
|
|
2
|
+
export default class CacheRedisProvider implements CacheStoreInterface {
|
|
3
|
+
private readonly client;
|
|
4
|
+
constructor(options: RedisCacheOptions);
|
|
5
|
+
/** Set any value (auto-stringifies objects) */
|
|
6
|
+
setValue(key: string, value: any, ttlSeconds?: number): Promise<void>;
|
|
7
|
+
/** Get a value and automatically parse JSON if possible */
|
|
8
|
+
getValue<T = any>(key: string, defaultValue?: T): Promise<T | undefined>;
|
|
9
|
+
/** Delete a key */
|
|
10
|
+
delete(key: string): Promise<void>;
|
|
11
|
+
/** Check if a key exists */
|
|
12
|
+
exists(key: string): Promise<boolean>;
|
|
13
|
+
/** Gracefully close the Redis connection */
|
|
14
|
+
close(): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const ioredis_1 = tslib_1.__importDefault(require("ioredis"));
|
|
5
|
+
const sdk_1 = require("@frontmcp/sdk");
|
|
6
|
+
let CacheRedisProvider = class CacheRedisProvider {
|
|
7
|
+
constructor(options) {
|
|
8
|
+
if (options.type !== 'redis' && options.type !== 'redis-client') {
|
|
9
|
+
throw new Error('Invalid cache provider type');
|
|
10
|
+
}
|
|
11
|
+
if (options.type === 'redis-client') {
|
|
12
|
+
this.client = options.client;
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
this.client = new ioredis_1.default({
|
|
16
|
+
lazyConnect: false,
|
|
17
|
+
maxRetriesPerRequest: 3,
|
|
18
|
+
...options.config,
|
|
19
|
+
});
|
|
20
|
+
this.client.on('connect', () => console.log('[Redis] Connected'));
|
|
21
|
+
this.client.on('error', (err) => console.error('[Redis] Error:', err));
|
|
22
|
+
}
|
|
23
|
+
/** Set any value (auto-stringifies objects) */
|
|
24
|
+
async setValue(key, value, ttlSeconds) {
|
|
25
|
+
const strValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
26
|
+
if (ttlSeconds && ttlSeconds > 0) {
|
|
27
|
+
await this.client.set(key, strValue, 'EX', ttlSeconds);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
await this.client.set(key, strValue);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Get a value and automatically parse JSON if possible */
|
|
34
|
+
async getValue(key, defaultValue) {
|
|
35
|
+
const raw = await this.client.get(key);
|
|
36
|
+
if (raw === null)
|
|
37
|
+
return defaultValue;
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(raw);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// fallback for plain string values
|
|
43
|
+
return raw;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Delete a key */
|
|
47
|
+
async delete(key) {
|
|
48
|
+
await this.client.del(key);
|
|
49
|
+
}
|
|
50
|
+
/** Check if a key exists */
|
|
51
|
+
async exists(key) {
|
|
52
|
+
return (await this.client.exists(key)) === 1;
|
|
53
|
+
}
|
|
54
|
+
/** Gracefully close the Redis connection */
|
|
55
|
+
async close() {
|
|
56
|
+
await this.client.quit();
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
CacheRedisProvider = tslib_1.__decorate([
|
|
60
|
+
(0, sdk_1.Provider)({
|
|
61
|
+
name: 'provider:cache:redis',
|
|
62
|
+
description: 'Redis-based cache provider',
|
|
63
|
+
scope: sdk_1.ProviderScope.GLOBAL,
|
|
64
|
+
}),
|
|
65
|
+
tslib_1.__metadata("design:paramtypes", [Object])
|
|
66
|
+
], CacheRedisProvider);
|
|
67
|
+
exports.default = CacheRedisProvider;
|
|
68
|
+
//# sourceMappingURL=cache-redis.provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-redis.provider.js","sourceRoot":"","sources":["../../../../src/cache/providers/cache-redis.provider.ts"],"names":[],"mappings":";;;AAAA,8DAAsD;AACtD,uCAAwD;AAQzC,IAAM,kBAAkB,GAAxB,MAAM,kBAAkB;IAGrC,YAAY,OAA0B;QACpC,IAAI,OAAO,CAAC,IAAI,KAAK,OAAO,IAAI,OAAO,CAAC,IAAI,KAAK,cAAc,EAAE,CAAC;YAChE,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC;QACjD,CAAC;QAED,IAAI,OAAO,CAAC,IAAI,KAAK,cAAc,EAAE,CAAC;YACpC,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;YAC7B,OAAO;QACT,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,IAAI,iBAAK,CAAC;YACtB,WAAW,EAAE,KAAK;YAClB,oBAAoB,EAAE,CAAC;YACvB,GAAG,OAAO,CAAC,MAAM;SAClB,CAAC,CAAC;QAEH,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC,CAAC;QAClE,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,gBAAgB,EAAE,GAAG,CAAC,CAAC,CAAC;IACzE,CAAC;IAED,+CAA+C;IAC/C,KAAK,CAAC,QAAQ,CAAC,GAAW,EAAE,KAAU,EAAE,UAAmB;QACzD,MAAM,QAAQ,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAC3E,IAAI,UAAU,IAAI,UAAU,GAAG,CAAC,EAAE,CAAC;YACjC,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC;QACzD,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QACvC,CAAC;IACH,CAAC;IAED,2DAA2D;IAC3D,KAAK,CAAC,QAAQ,CAAU,GAAW,EAAE,YAAgB;QACnD,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACvC,IAAI,GAAG,KAAK,IAAI;YAAE,OAAO,YAAY,CAAC;QAEtC,IAAI,CAAC;YACH,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAM,CAAC;QAC9B,CAAC;QAAC,MAAM,CAAC;YACP,mCAAmC;YACnC,OAAO,GAAmB,CAAC;QAC7B,CAAC;IACH,CAAC;IAED,mBAAmB;IACnB,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC7B,CAAC;IAED,4BAA4B;IAC5B,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,OAAO,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;IAC/C,CAAC;IAED,4CAA4C;IAC5C,KAAK,CAAC,KAAK;QACT,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IAC3B,CAAC;CACF,CAAA;AA3DoB,kBAAkB;IALtC,IAAA,cAAQ,EAAC;QACR,IAAI,EAAE,sBAAsB;QAC5B,WAAW,EAAE,4BAA4B;QACzC,KAAK,EAAE,mBAAa,CAAC,MAAM;KAC5B,CAAC;;GACmB,kBAAkB,CA2DtC;kBA3DoB,kBAAkB","sourcesContent":["import Redis, { Redis as RedisClient } from 'ioredis';\nimport { Provider, ProviderScope } from '@frontmcp/sdk';\nimport { CacheStoreInterface, RedisCacheOptions } from '../cache.types';\n\n@Provider({\n name: 'provider:cache:redis',\n description: 'Redis-based cache provider',\n scope: ProviderScope.GLOBAL,\n})\nexport default class CacheRedisProvider implements CacheStoreInterface {\n private readonly client: RedisClient;\n\n constructor(options: RedisCacheOptions) {\n if (options.type !== 'redis' && options.type !== 'redis-client') {\n throw new Error('Invalid cache provider type');\n }\n\n if (options.type === 'redis-client') {\n this.client = options.client;\n return;\n }\n this.client = new Redis({\n lazyConnect: false,\n maxRetriesPerRequest: 3,\n ...options.config,\n });\n\n this.client.on('connect', () => console.log('[Redis] Connected'));\n this.client.on('error', (err) => console.error('[Redis] Error:', err));\n }\n\n /** Set any value (auto-stringifies objects) */\n async setValue(key: string, value: any, ttlSeconds?: number): Promise<void> {\n const strValue = typeof value === 'string' ? value : JSON.stringify(value);\n if (ttlSeconds && ttlSeconds > 0) {\n await this.client.set(key, strValue, 'EX', ttlSeconds);\n } else {\n await this.client.set(key, strValue);\n }\n }\n\n /** Get a value and automatically parse JSON if possible */\n async getValue<T = any>(key: string, defaultValue?: T): Promise<T | undefined> {\n const raw = await this.client.get(key);\n if (raw === null) return defaultValue;\n\n try {\n return JSON.parse(raw) as T;\n } catch {\n // fallback for plain string values\n return raw as unknown as T;\n }\n }\n\n /** Delete a key */\n async delete(key: string): Promise<void> {\n await this.client.del(key);\n }\n\n /** Check if a key exists */\n async exists(key: string): Promise<boolean> {\n return (await this.client.exists(key)) === 1;\n }\n\n /** Gracefully close the Redis connection */\n async close(): Promise<void> {\n await this.client.quit();\n }\n}\n"]}
|