@frontmcp/sdk 0.6.0 → 0.6.1
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 +1 -0
- package/package.json +13 -6
- package/src/auth/session/index.d.ts +1 -0
- package/src/auth/session/index.js +3 -1
- package/src/auth/session/index.js.map +1 -1
- package/src/auth/session/vercel-kv-session.store.d.ts +96 -0
- package/src/auth/session/vercel-kv-session.store.js +216 -0
- package/src/auth/session/vercel-kv-session.store.js.map +1 -0
- package/src/common/decorators/front-mcp.decorator.js +14 -17
- package/src/common/decorators/front-mcp.decorator.js.map +1 -1
- package/src/common/metadata/front-mcp.metadata.d.ts +705 -23
- package/src/common/metadata/front-mcp.metadata.js +1 -0
- package/src/common/metadata/front-mcp.metadata.js.map +1 -1
- package/src/common/metadata/prompt.metadata.d.ts +4 -0
- package/src/common/metadata/resource.metadata.d.ts +8 -0
- package/src/common/metadata/tool-ui.metadata.d.ts +2 -2
- package/src/common/metadata/tool-ui.metadata.js +1 -1
- package/src/common/metadata/tool-ui.metadata.js.map +1 -1
- package/src/common/metadata/tool.metadata.d.ts +4 -0
- package/src/common/schemas/http-output.schema.d.ts +24 -6
- package/src/common/tokens/front-mcp.tokens.js +1 -0
- package/src/common/tokens/front-mcp.tokens.js.map +1 -1
- package/src/common/types/options/redis.options.d.ts +173 -5
- package/src/common/types/options/redis.options.js +157 -11
- package/src/common/types/options/redis.options.js.map +1 -1
- package/src/common/types/options/server-info.options.d.ts +4 -0
- package/src/common/types/options/transport.options.d.ts +68 -4
- package/src/common/utils/global-config.utils.d.ts +36 -0
- package/src/common/utils/global-config.utils.js +44 -0
- package/src/common/utils/global-config.utils.js.map +1 -0
- package/src/common/utils/index.d.ts +1 -0
- package/src/common/utils/index.js +1 -0
- package/src/common/utils/index.js.map +1 -1
- package/src/completion/flows/complete.flow.d.ts +6 -8
- package/src/errors/index.d.ts +1 -1
- package/src/errors/index.js +2 -1
- package/src/errors/index.js.map +1 -1
- package/src/errors/mcp.error.d.ts +9 -0
- package/src/errors/mcp.error.js +19 -1
- package/src/errors/mcp.error.js.map +1 -1
- package/src/front-mcp/front-mcp.providers.d.ts +208 -0
- package/src/front-mcp/index.d.ts +1 -0
- package/src/front-mcp/index.js +3 -0
- package/src/front-mcp/index.js.map +1 -1
- package/src/index.d.ts +1 -1
- package/src/index.js +2 -1
- package/src/index.js.map +1 -1
- package/src/logging/flows/set-level.flow.d.ts +6 -8
- package/src/prompt/flows/get-prompt.flow.d.ts +14 -8
- package/src/prompt/flows/prompts-list.flow.d.ts +8 -7
- package/src/resource/flows/read-resource.flow.d.ts +8 -9
- package/src/resource/flows/resource-templates-list.flow.d.ts +8 -7
- package/src/resource/flows/resources-list.flow.d.ts +8 -7
- package/src/resource/flows/subscribe-resource.flow.d.ts +6 -8
- package/src/resource/flows/unsubscribe-resource.flow.d.ts +6 -8
- package/src/store/adapters/store.vercel-kv.adapter.d.ts +86 -0
- package/src/store/adapters/store.vercel-kv.adapter.js +155 -0
- package/src/store/adapters/store.vercel-kv.adapter.js.map +1 -0
- package/src/store/index.d.ts +2 -0
- package/src/store/index.js +2 -0
- package/src/store/index.js.map +1 -1
- package/src/store/store.factory.d.ts +86 -0
- package/src/store/store.factory.js +194 -0
- package/src/store/store.factory.js.map +1 -0
- package/src/tool/flows/call-tool.flow.d.ts +18 -9
- package/src/tool/flows/call-tool.flow.js +2 -2
- package/src/tool/flows/call-tool.flow.js.map +1 -1
- package/src/tool/flows/tools-list.flow.d.ts +9 -8
- package/src/tool/flows/tools-list.flow.js +2 -2
- package/src/tool/flows/tools-list.flow.js.map +1 -1
- package/src/tool/ui/index.d.ts +4 -4
- package/src/tool/ui/index.js +4 -4
- package/src/tool/ui/index.js.map +1 -1
- package/src/tool/ui/platform-adapters.d.ts +2 -2
- package/src/tool/ui/platform-adapters.js +3 -3
- package/src/tool/ui/platform-adapters.js.map +1 -1
- package/src/tool/ui/template-helpers.d.ts +5 -7
- package/src/tool/ui/template-helpers.js +9 -26
- package/src/tool/ui/template-helpers.js.map +1 -1
- package/src/tool/ui/ui-resource.handler.d.ts +1 -1
- package/src/tool/ui/ui-resource.handler.js +5 -5
- package/src/tool/ui/ui-resource.handler.js.map +1 -1
- package/src/transport/mcp-handlers/complete-request.handler.d.ts +4 -15
- package/src/transport/mcp-handlers/get-prompt-request.handler.d.ts +5 -15
- package/src/transport/mcp-handlers/index.d.ts +67 -195
- package/src/transport/mcp-handlers/list-prompts-request.handler.d.ts +5 -15
- package/src/transport/mcp-handlers/list-resource-templates-request.handler.d.ts +5 -15
- package/src/transport/mcp-handlers/list-resources-request.handler.d.ts +5 -15
- package/src/transport/mcp-handlers/list-tools-request.handler.d.ts +5 -15
- package/src/transport/mcp-handlers/logging-set-level-request.handler.d.ts +3 -14
- package/src/transport/mcp-handlers/read-resource-request.handler.d.ts +4 -15
- package/src/transport/mcp-handlers/subscribe-request.handler.d.ts +3 -14
- package/src/transport/mcp-handlers/unsubscribe-request.handler.d.ts +3 -14
- package/src/transport/transport.registry.d.ts +5 -1
- package/src/transport/transport.registry.js +52 -23
- package/src/transport/transport.registry.js.map +1 -1
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ _Made with ❤️ for TypeScript developers_
|
|
|
14
14
|
[](https://www.npmjs.com/package/@frontmcp/sdk)
|
|
15
15
|
[](https://nodejs.org)
|
|
16
16
|
[](https://github.com/agentfront/frontmcp/blob/main/LICENSE)
|
|
17
|
+
[](https://snyk.io/test/github/agentfront/frontmcp)
|
|
17
18
|
|
|
18
19
|
</div>
|
|
19
20
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/sdk",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "FrontMCP SDK",
|
|
5
5
|
"author": "AgentFront <info@agentfront.dev>",
|
|
6
6
|
"homepage": "https://docs.agentfront.dev",
|
|
@@ -40,19 +40,26 @@
|
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"zod": "^4.0.0",
|
|
43
|
-
"express": "^4.
|
|
43
|
+
"express": "^4.18.0 || ^5.0.0",
|
|
44
44
|
"cors": "^2.8.5",
|
|
45
45
|
"raw-body": "^3.0.0",
|
|
46
|
-
"content-type": "^1.0.5"
|
|
46
|
+
"content-type": "^1.0.5",
|
|
47
|
+
"@vercel/kv": "^2.0.0 || ^3.0.0"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"@vercel/kv": {
|
|
51
|
+
"optional": true
|
|
52
|
+
}
|
|
47
53
|
},
|
|
48
54
|
"dependencies": {
|
|
49
|
-
"@frontmcp/
|
|
50
|
-
"@modelcontextprotocol/sdk": "1.
|
|
55
|
+
"@frontmcp/uipack": "0.6.1",
|
|
56
|
+
"@modelcontextprotocol/sdk": "1.25.1",
|
|
51
57
|
"ioredis": "^5.8.0",
|
|
52
|
-
"jose": "^6.1.
|
|
58
|
+
"jose": "^6.1.3",
|
|
53
59
|
"reflect-metadata": "^0.2.2"
|
|
54
60
|
},
|
|
55
61
|
"devDependencies": {
|
|
62
|
+
"@vercel/kv": "^3.0.0",
|
|
56
63
|
"typescript": "^5.9.3"
|
|
57
64
|
},
|
|
58
65
|
"type": "commonjs"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from './transport-session.types';
|
|
2
2
|
export { TransportSessionManager, InMemorySessionStore } from './transport-session.manager';
|
|
3
3
|
export { RedisSessionStore } from './redis-session.store';
|
|
4
|
+
export { VercelKvSessionStore } from './vercel-kv-session.store';
|
|
4
5
|
export * from './authorization.store';
|
|
5
6
|
export * from './authorization-vault';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.RedisSessionStore = exports.InMemorySessionStore = exports.TransportSessionManager = void 0;
|
|
3
|
+
exports.VercelKvSessionStore = exports.RedisSessionStore = exports.InMemorySessionStore = exports.TransportSessionManager = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
// Transport session architecture
|
|
6
6
|
tslib_1.__exportStar(require("./transport-session.types"), exports);
|
|
@@ -9,6 +9,8 @@ Object.defineProperty(exports, "TransportSessionManager", { enumerable: true, ge
|
|
|
9
9
|
Object.defineProperty(exports, "InMemorySessionStore", { enumerable: true, get: function () { return transport_session_manager_1.InMemorySessionStore; } });
|
|
10
10
|
var redis_session_store_1 = require("./redis-session.store");
|
|
11
11
|
Object.defineProperty(exports, "RedisSessionStore", { enumerable: true, get: function () { return redis_session_store_1.RedisSessionStore; } });
|
|
12
|
+
var vercel_kv_session_store_1 = require("./vercel-kv-session.store");
|
|
13
|
+
Object.defineProperty(exports, "VercelKvSessionStore", { enumerable: true, get: function () { return vercel_kv_session_store_1.VercelKvSessionStore; } });
|
|
12
14
|
// Authorization store for OAuth flows
|
|
13
15
|
tslib_1.__exportStar(require("./authorization.store"), exports);
|
|
14
16
|
// Authorization vault for stateful sessions
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/auth/session/index.ts"],"names":[],"mappings":";;;;AAAA,iCAAiC;AACjC,oEAA0C;AAC1C,yEAA4F;AAAnF,oIAAA,uBAAuB,OAAA;AAAE,iIAAA,oBAAoB,OAAA;AACtD,6DAA0D;AAAjD,wHAAA,iBAAiB,OAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/auth/session/index.ts"],"names":[],"mappings":";;;;AAAA,iCAAiC;AACjC,oEAA0C;AAC1C,yEAA4F;AAAnF,oIAAA,uBAAuB,OAAA;AAAE,iIAAA,oBAAoB,OAAA;AACtD,6DAA0D;AAAjD,wHAAA,iBAAiB,OAAA;AAC1B,qEAAiE;AAAxD,+HAAA,oBAAoB,OAAA;AAE7B,sCAAsC;AACtC,gEAAsC;AAEtC,4CAA4C;AAC5C,gEAAsC","sourcesContent":["// Transport session architecture\nexport * from './transport-session.types';\nexport { TransportSessionManager, InMemorySessionStore } from './transport-session.manager';\nexport { RedisSessionStore } from './redis-session.store';\nexport { VercelKvSessionStore } from './vercel-kv-session.store';\n\n// Authorization store for OAuth flows\nexport * from './authorization.store';\n\n// Authorization vault for stateful sessions\nexport * from './authorization-vault';\n"]}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vercel KV Session Store
|
|
3
|
+
*
|
|
4
|
+
* Session store implementation using Vercel KV (edge-compatible REST-based key-value store).
|
|
5
|
+
* Uses dynamic import to avoid bundling @vercel/kv for non-Vercel deployments.
|
|
6
|
+
*
|
|
7
|
+
* @see https://vercel.com/docs/storage/vercel-kv
|
|
8
|
+
*/
|
|
9
|
+
import { SessionStore, StoredSession } from './transport-session.types';
|
|
10
|
+
import { FrontMcpLogger } from '../../common/interfaces/logger.interface';
|
|
11
|
+
import type { VercelKvProviderOptions } from '../../common/types/options/redis.options';
|
|
12
|
+
export interface VercelKvSessionConfig {
|
|
13
|
+
/**
|
|
14
|
+
* KV REST API URL
|
|
15
|
+
* @default process.env.KV_REST_API_URL
|
|
16
|
+
*/
|
|
17
|
+
url?: string;
|
|
18
|
+
/**
|
|
19
|
+
* KV REST API Token
|
|
20
|
+
* @default process.env.KV_REST_API_TOKEN
|
|
21
|
+
*/
|
|
22
|
+
token?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Key prefix for session keys
|
|
25
|
+
* @default 'mcp:session:'
|
|
26
|
+
*/
|
|
27
|
+
keyPrefix?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Default TTL in milliseconds for session extension on access
|
|
30
|
+
* @default 3600000 (1 hour)
|
|
31
|
+
*/
|
|
32
|
+
defaultTtlMs?: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Vercel KV-backed session store implementation
|
|
36
|
+
*
|
|
37
|
+
* Provides persistent session storage for edge deployments using Vercel KV.
|
|
38
|
+
* Sessions are stored as JSON with optional TTL.
|
|
39
|
+
*/
|
|
40
|
+
export declare class VercelKvSessionStore implements SessionStore {
|
|
41
|
+
private kv;
|
|
42
|
+
private connectPromise;
|
|
43
|
+
private readonly keyPrefix;
|
|
44
|
+
private readonly defaultTtlMs;
|
|
45
|
+
private readonly logger?;
|
|
46
|
+
private readonly config;
|
|
47
|
+
constructor(config: VercelKvSessionConfig | VercelKvProviderOptions, logger?: FrontMcpLogger);
|
|
48
|
+
/**
|
|
49
|
+
* Connect to Vercel KV
|
|
50
|
+
* Uses dynamic import to avoid bundling @vercel/kv when not used.
|
|
51
|
+
* Thread-safe: concurrent calls will share the same connection promise.
|
|
52
|
+
*/
|
|
53
|
+
connect(): Promise<void>;
|
|
54
|
+
private doConnect;
|
|
55
|
+
private ensureConnected;
|
|
56
|
+
/**
|
|
57
|
+
* Get the full key for a session ID
|
|
58
|
+
* @throws Error if sessionId is empty
|
|
59
|
+
*/
|
|
60
|
+
private key;
|
|
61
|
+
/**
|
|
62
|
+
* Get a stored session by ID
|
|
63
|
+
*
|
|
64
|
+
* Note: Vercel KV doesn't support GETEX, so we use GET + PEXPIRE separately.
|
|
65
|
+
* This is slightly less atomic than Redis GETEX but sufficient for most use cases.
|
|
66
|
+
*/
|
|
67
|
+
get(sessionId: string): Promise<StoredSession | null>;
|
|
68
|
+
/**
|
|
69
|
+
* Store a session with optional TTL
|
|
70
|
+
*/
|
|
71
|
+
set(sessionId: string, session: StoredSession, ttlMs?: number): Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* Delete a session
|
|
74
|
+
*/
|
|
75
|
+
delete(sessionId: string): Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* Check if a session exists
|
|
78
|
+
*/
|
|
79
|
+
exists(sessionId: string): Promise<boolean>;
|
|
80
|
+
/**
|
|
81
|
+
* Allocate a new session ID
|
|
82
|
+
*/
|
|
83
|
+
allocId(): string;
|
|
84
|
+
/**
|
|
85
|
+
* Disconnect from Vercel KV
|
|
86
|
+
* Vercel KV uses REST API, so there's no persistent connection to close
|
|
87
|
+
*/
|
|
88
|
+
disconnect(): Promise<void>;
|
|
89
|
+
/**
|
|
90
|
+
* Test Vercel KV connection by setting and getting a test key.
|
|
91
|
+
* Useful for validating connection on startup.
|
|
92
|
+
*
|
|
93
|
+
* @returns true if connection is healthy, false otherwise
|
|
94
|
+
*/
|
|
95
|
+
ping(): Promise<boolean>;
|
|
96
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Vercel KV Session Store
|
|
4
|
+
*
|
|
5
|
+
* Session store implementation using Vercel KV (edge-compatible REST-based key-value store).
|
|
6
|
+
* Uses dynamic import to avoid bundling @vercel/kv for non-Vercel deployments.
|
|
7
|
+
*
|
|
8
|
+
* @see https://vercel.com/docs/storage/vercel-kv
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.VercelKvSessionStore = void 0;
|
|
12
|
+
const crypto_1 = require("crypto");
|
|
13
|
+
const transport_session_types_1 = require("./transport-session.types");
|
|
14
|
+
/**
|
|
15
|
+
* Vercel KV-backed session store implementation
|
|
16
|
+
*
|
|
17
|
+
* Provides persistent session storage for edge deployments using Vercel KV.
|
|
18
|
+
* Sessions are stored as JSON with optional TTL.
|
|
19
|
+
*/
|
|
20
|
+
class VercelKvSessionStore {
|
|
21
|
+
kv = null;
|
|
22
|
+
connectPromise = null;
|
|
23
|
+
keyPrefix;
|
|
24
|
+
defaultTtlMs;
|
|
25
|
+
logger;
|
|
26
|
+
config;
|
|
27
|
+
constructor(config, logger) {
|
|
28
|
+
this.config = config;
|
|
29
|
+
this.keyPrefix = config.keyPrefix ?? 'mcp:session:';
|
|
30
|
+
this.defaultTtlMs = config.defaultTtlMs ?? 3600000;
|
|
31
|
+
this.logger = logger;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Connect to Vercel KV
|
|
35
|
+
* Uses dynamic import to avoid bundling @vercel/kv when not used.
|
|
36
|
+
* Thread-safe: concurrent calls will share the same connection promise.
|
|
37
|
+
*/
|
|
38
|
+
async connect() {
|
|
39
|
+
if (this.kv)
|
|
40
|
+
return;
|
|
41
|
+
// Prevent concurrent connection attempts
|
|
42
|
+
if (this.connectPromise) {
|
|
43
|
+
return this.connectPromise;
|
|
44
|
+
}
|
|
45
|
+
this.connectPromise = this.doConnect();
|
|
46
|
+
try {
|
|
47
|
+
await this.connectPromise;
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
// Reset promise on failure to allow retry
|
|
51
|
+
this.connectPromise = null;
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async doConnect() {
|
|
56
|
+
const { createClient } = await import('@vercel/kv');
|
|
57
|
+
const url = this.config.url || process.env['KV_REST_API_URL'];
|
|
58
|
+
const token = this.config.token || process.env['KV_REST_API_TOKEN'];
|
|
59
|
+
if (!url || !token) {
|
|
60
|
+
throw new Error('Vercel KV requires url and token. Set KV_REST_API_URL and KV_REST_API_TOKEN environment variables or provide them in config.');
|
|
61
|
+
}
|
|
62
|
+
// Cast to our interface to avoid type compatibility issues
|
|
63
|
+
this.kv = createClient({ url, token });
|
|
64
|
+
}
|
|
65
|
+
async ensureConnected() {
|
|
66
|
+
await this.connect();
|
|
67
|
+
if (!this.kv) {
|
|
68
|
+
throw new Error('[VercelKvSessionStore] Connection failed - client not initialized');
|
|
69
|
+
}
|
|
70
|
+
return this.kv;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Get the full key for a session ID
|
|
74
|
+
* @throws Error if sessionId is empty
|
|
75
|
+
*/
|
|
76
|
+
key(sessionId) {
|
|
77
|
+
if (!sessionId || sessionId.trim() === '') {
|
|
78
|
+
throw new Error('[VercelKvSessionStore] sessionId cannot be empty');
|
|
79
|
+
}
|
|
80
|
+
return `${this.keyPrefix}${sessionId}`;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Get a stored session by ID
|
|
84
|
+
*
|
|
85
|
+
* Note: Vercel KV doesn't support GETEX, so we use GET + PEXPIRE separately.
|
|
86
|
+
* This is slightly less atomic than Redis GETEX but sufficient for most use cases.
|
|
87
|
+
*/
|
|
88
|
+
async get(sessionId) {
|
|
89
|
+
const kv = await this.ensureConnected();
|
|
90
|
+
const key = this.key(sessionId);
|
|
91
|
+
// Get the session
|
|
92
|
+
const raw = await kv.get(key);
|
|
93
|
+
if (!raw)
|
|
94
|
+
return null;
|
|
95
|
+
// Extend TTL (fire-and-forget, similar to Redis GETEX behavior)
|
|
96
|
+
kv.pexpire(key, this.defaultTtlMs).catch(() => void 0);
|
|
97
|
+
try {
|
|
98
|
+
const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
99
|
+
const result = transport_session_types_1.storedSessionSchema.safeParse(parsed);
|
|
100
|
+
if (!result.success) {
|
|
101
|
+
this.logger?.warn('[VercelKvSessionStore] Invalid session format', {
|
|
102
|
+
sessionId: sessionId.slice(0, 20),
|
|
103
|
+
errors: result.error.issues.slice(0, 3).map((i) => ({ path: i.path, message: i.message })),
|
|
104
|
+
});
|
|
105
|
+
// Delete invalid session data
|
|
106
|
+
this.delete(sessionId).catch(() => void 0);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const session = result.data;
|
|
110
|
+
// Check application-level expiration (separate from KV TTL)
|
|
111
|
+
if (session.session.expiresAt && session.session.expiresAt < Date.now()) {
|
|
112
|
+
// Session is logically expired - delete it
|
|
113
|
+
await this.delete(sessionId);
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
// Bound TTL by session.expiresAt to avoid keeping expired sessions
|
|
117
|
+
if (session.session.expiresAt) {
|
|
118
|
+
const ttlMs = Math.min(this.defaultTtlMs, session.session.expiresAt - Date.now());
|
|
119
|
+
if (ttlMs > 0 && ttlMs < this.defaultTtlMs) {
|
|
120
|
+
// Fire-and-forget - we're only optimizing cache eviction timing
|
|
121
|
+
kv.pexpire(key, ttlMs).catch(() => void 0);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Update last accessed timestamp (in the returned object)
|
|
125
|
+
const updatedSession = {
|
|
126
|
+
...session,
|
|
127
|
+
lastAccessedAt: Date.now(),
|
|
128
|
+
};
|
|
129
|
+
return updatedSession;
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
this.logger?.warn('[VercelKvSessionStore] Failed to parse session', {
|
|
133
|
+
sessionId: sessionId.slice(0, 20),
|
|
134
|
+
error: error.message,
|
|
135
|
+
});
|
|
136
|
+
// Delete corrupted session payloads to prevent repeated failures
|
|
137
|
+
this.delete(sessionId).catch(() => void 0);
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Store a session with optional TTL
|
|
143
|
+
*/
|
|
144
|
+
async set(sessionId, session, ttlMs) {
|
|
145
|
+
const kv = await this.ensureConnected();
|
|
146
|
+
const key = this.key(sessionId);
|
|
147
|
+
const value = JSON.stringify(session);
|
|
148
|
+
if (ttlMs && ttlMs > 0) {
|
|
149
|
+
// Use px for millisecond precision
|
|
150
|
+
await kv.set(key, value, { px: ttlMs });
|
|
151
|
+
}
|
|
152
|
+
else if (session.session.expiresAt) {
|
|
153
|
+
// Use session's expiration if available
|
|
154
|
+
const ttl = session.session.expiresAt - Date.now();
|
|
155
|
+
if (ttl > 0) {
|
|
156
|
+
await kv.set(key, value, { px: ttl });
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
// Already expired, but store anyway (will be cleaned up on next access)
|
|
160
|
+
await kv.set(key, value);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
// No TTL - session persists until explicitly deleted
|
|
165
|
+
await kv.set(key, value);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Delete a session
|
|
170
|
+
*/
|
|
171
|
+
async delete(sessionId) {
|
|
172
|
+
const kv = await this.ensureConnected();
|
|
173
|
+
await kv.del(this.key(sessionId));
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Check if a session exists
|
|
177
|
+
*/
|
|
178
|
+
async exists(sessionId) {
|
|
179
|
+
const kv = await this.ensureConnected();
|
|
180
|
+
return (await kv.exists(this.key(sessionId))) === 1;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Allocate a new session ID
|
|
184
|
+
*/
|
|
185
|
+
allocId() {
|
|
186
|
+
return (0, crypto_1.randomUUID)();
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Disconnect from Vercel KV
|
|
190
|
+
* Vercel KV uses REST API, so there's no persistent connection to close
|
|
191
|
+
*/
|
|
192
|
+
async disconnect() {
|
|
193
|
+
this.kv = null;
|
|
194
|
+
this.connectPromise = null;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Test Vercel KV connection by setting and getting a test key.
|
|
198
|
+
* Useful for validating connection on startup.
|
|
199
|
+
*
|
|
200
|
+
* @returns true if connection is healthy, false otherwise
|
|
201
|
+
*/
|
|
202
|
+
async ping() {
|
|
203
|
+
try {
|
|
204
|
+
const kv = await this.ensureConnected();
|
|
205
|
+
const testKey = `${this.keyPrefix}__ping__`;
|
|
206
|
+
await kv.set(testKey, 'pong', { ex: 1 });
|
|
207
|
+
const result = await kv.get(testKey);
|
|
208
|
+
return result === 'pong';
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
exports.VercelKvSessionStore = VercelKvSessionStore;
|
|
216
|
+
//# sourceMappingURL=vercel-kv-session.store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vercel-kv-session.store.js","sourceRoot":"","sources":["../../../../src/auth/session/vercel-kv-session.store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Vercel KV Session Store\n *\n * Session store implementation using Vercel KV (edge-compatible REST-based key-value store).\n * Uses dynamic import to avoid bundling @vercel/kv for non-Vercel deployments.\n *\n * @see https://vercel.com/docs/storage/vercel-kv\n */\n\nimport { randomUUID } from 'crypto';\nimport { SessionStore, StoredSession, storedSessionSchema } from './transport-session.types';\nimport { FrontMcpLogger } from '../../common/interfaces/logger.interface';\nimport type { VercelKvProviderOptions } from '../../common/types/options/redis.options';\n\n// Interface for the Vercel KV client (matches @vercel/kv API)\n// Using custom interface to avoid type compatibility issues with optional dependency\ninterface VercelKVClient {\n get<T = unknown>(key: string): Promise<T | null>;\n set(key: string, value: unknown, options?: { ex?: number; px?: number }): Promise<void>;\n del(...keys: string[]): Promise<number>;\n exists(...keys: string[]): Promise<number>;\n pexpire(key: string, milliseconds: number): Promise<number>;\n}\n\nexport interface VercelKvSessionConfig {\n /**\n * KV REST API URL\n * @default process.env.KV_REST_API_URL\n */\n url?: string;\n\n /**\n * KV REST API Token\n * @default process.env.KV_REST_API_TOKEN\n */\n token?: string;\n\n /**\n * Key prefix for session keys\n * @default 'mcp:session:'\n */\n keyPrefix?: string;\n\n /**\n * Default TTL in milliseconds for session extension on access\n * @default 3600000 (1 hour)\n */\n defaultTtlMs?: number;\n}\n\n/**\n * Vercel KV-backed session store implementation\n *\n * Provides persistent session storage for edge deployments using Vercel KV.\n * Sessions are stored as JSON with optional TTL.\n */\nexport class VercelKvSessionStore implements SessionStore {\n private kv: VercelKVClient | null = null;\n private connectPromise: Promise<void> | null = null;\n private readonly keyPrefix: string;\n private readonly defaultTtlMs: number;\n private readonly logger?: FrontMcpLogger;\n private readonly config: VercelKvSessionConfig;\n\n constructor(config: VercelKvSessionConfig | VercelKvProviderOptions, logger?: FrontMcpLogger) {\n this.config = config;\n this.keyPrefix = config.keyPrefix ?? 'mcp:session:';\n this.defaultTtlMs = config.defaultTtlMs ?? 3600000;\n this.logger = logger;\n }\n\n /**\n * Connect to Vercel KV\n * Uses dynamic import to avoid bundling @vercel/kv when not used.\n * Thread-safe: concurrent calls will share the same connection promise.\n */\n async connect(): Promise<void> {\n if (this.kv) return;\n\n // Prevent concurrent connection attempts\n if (this.connectPromise) {\n return this.connectPromise;\n }\n\n this.connectPromise = this.doConnect();\n try {\n await this.connectPromise;\n } catch (error) {\n // Reset promise on failure to allow retry\n this.connectPromise = null;\n throw error;\n }\n }\n\n private async doConnect(): Promise<void> {\n const { createClient } = await import('@vercel/kv');\n\n const url = this.config.url || process.env['KV_REST_API_URL'];\n const token = this.config.token || process.env['KV_REST_API_TOKEN'];\n\n if (!url || !token) {\n throw new Error(\n 'Vercel KV requires url and token. Set KV_REST_API_URL and KV_REST_API_TOKEN environment variables or provide them in config.',\n );\n }\n\n // Cast to our interface to avoid type compatibility issues\n this.kv = createClient({ url, token }) as unknown as VercelKVClient;\n }\n\n private async ensureConnected(): Promise<VercelKVClient> {\n await this.connect();\n if (!this.kv) {\n throw new Error('[VercelKvSessionStore] Connection failed - client not initialized');\n }\n return this.kv;\n }\n\n /**\n * Get the full key for a session ID\n * @throws Error if sessionId is empty\n */\n private key(sessionId: string): string {\n if (!sessionId || sessionId.trim() === '') {\n throw new Error('[VercelKvSessionStore] sessionId cannot be empty');\n }\n return `${this.keyPrefix}${sessionId}`;\n }\n\n /**\n * Get a stored session by ID\n *\n * Note: Vercel KV doesn't support GETEX, so we use GET + PEXPIRE separately.\n * This is slightly less atomic than Redis GETEX but sufficient for most use cases.\n */\n async get(sessionId: string): Promise<StoredSession | null> {\n const kv = await this.ensureConnected();\n const key = this.key(sessionId);\n\n // Get the session\n const raw = await kv.get<string>(key);\n if (!raw) return null;\n\n // Extend TTL (fire-and-forget, similar to Redis GETEX behavior)\n kv.pexpire(key, this.defaultTtlMs).catch(() => void 0);\n\n try {\n const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;\n const result = storedSessionSchema.safeParse(parsed);\n\n if (!result.success) {\n this.logger?.warn('[VercelKvSessionStore] Invalid session format', {\n sessionId: sessionId.slice(0, 20),\n errors: result.error.issues.slice(0, 3).map((i) => ({ path: i.path, message: i.message })),\n });\n // Delete invalid session data\n this.delete(sessionId).catch(() => void 0);\n return null;\n }\n\n const session = result.data;\n\n // Check application-level expiration (separate from KV TTL)\n if (session.session.expiresAt && session.session.expiresAt < Date.now()) {\n // Session is logically expired - delete it\n await this.delete(sessionId);\n return null;\n }\n\n // Bound TTL by session.expiresAt to avoid keeping expired sessions\n if (session.session.expiresAt) {\n const ttlMs = Math.min(this.defaultTtlMs, session.session.expiresAt - Date.now());\n if (ttlMs > 0 && ttlMs < this.defaultTtlMs) {\n // Fire-and-forget - we're only optimizing cache eviction timing\n kv.pexpire(key, ttlMs).catch(() => void 0);\n }\n }\n\n // Update last accessed timestamp (in the returned object)\n const updatedSession: StoredSession = {\n ...session,\n lastAccessedAt: Date.now(),\n };\n\n return updatedSession;\n } catch (error) {\n this.logger?.warn('[VercelKvSessionStore] Failed to parse session', {\n sessionId: sessionId.slice(0, 20),\n error: (error as Error).message,\n });\n // Delete corrupted session payloads to prevent repeated failures\n this.delete(sessionId).catch(() => void 0);\n return null;\n }\n }\n\n /**\n * Store a session with optional TTL\n */\n async set(sessionId: string, session: StoredSession, ttlMs?: number): Promise<void> {\n const kv = await this.ensureConnected();\n const key = this.key(sessionId);\n const value = JSON.stringify(session);\n\n if (ttlMs && ttlMs > 0) {\n // Use px for millisecond precision\n await kv.set(key, value, { px: ttlMs });\n } else if (session.session.expiresAt) {\n // Use session's expiration if available\n const ttl = session.session.expiresAt - Date.now();\n if (ttl > 0) {\n await kv.set(key, value, { px: ttl });\n } else {\n // Already expired, but store anyway (will be cleaned up on next access)\n await kv.set(key, value);\n }\n } else {\n // No TTL - session persists until explicitly deleted\n await kv.set(key, value);\n }\n }\n\n /**\n * Delete a session\n */\n async delete(sessionId: string): Promise<void> {\n const kv = await this.ensureConnected();\n await kv.del(this.key(sessionId));\n }\n\n /**\n * Check if a session exists\n */\n async exists(sessionId: string): Promise<boolean> {\n const kv = await this.ensureConnected();\n return (await kv.exists(this.key(sessionId))) === 1;\n }\n\n /**\n * Allocate a new session ID\n */\n allocId(): string {\n return randomUUID();\n }\n\n /**\n * Disconnect from Vercel KV\n * Vercel KV uses REST API, so there's no persistent connection to close\n */\n async disconnect(): Promise<void> {\n this.kv = null;\n this.connectPromise = null;\n }\n\n /**\n * Test Vercel KV connection by setting and getting a test key.\n * Useful for validating connection on startup.\n *\n * @returns true if connection is healthy, false otherwise\n */\n async ping(): Promise<boolean> {\n try {\n const kv = await this.ensureConnected();\n const testKey = `${this.keyPrefix}__ping__`;\n await kv.set(testKey, 'pong', { ex: 1 });\n const result = await kv.get<string>(testKey);\n return result === 'pong';\n } catch {\n return false;\n }\n }\n}\n"]}
|
|
@@ -5,6 +5,7 @@ require("reflect-metadata");
|
|
|
5
5
|
const tokens_1 = require("../tokens");
|
|
6
6
|
const metadata_1 = require("../metadata");
|
|
7
7
|
const migrate_1 = require("../migrate");
|
|
8
|
+
const mcp_error_1 = require("../../errors/mcp.error");
|
|
8
9
|
/**
|
|
9
10
|
* Decorator that marks a class as a FrontMcp Server and provides metadata
|
|
10
11
|
*/
|
|
@@ -34,22 +35,18 @@ function FrontMcp(providedMetadata) {
|
|
|
34
35
|
const isServerless = typeof process !== 'undefined' && process.env?.['FRONTMCP_SERVERLESS'] === '1';
|
|
35
36
|
if (isServerless) {
|
|
36
37
|
// Serverless mode: bootstrap, prepare (no listen), store handler globally
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
});
|
|
50
|
-
})
|
|
51
|
-
.catch((err) => {
|
|
52
|
-
console.error('[FrontMCP] Failed to import @frontmcp/sdk for serverless init:', err);
|
|
38
|
+
// Use synchronous require for bundler compatibility (rspack/webpack)
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
40
|
+
const { FrontMcpInstance: ServerlessInstance, setServerlessHandler, setServerlessHandlerPromise, setServerlessHandlerError, } = require('@frontmcp/sdk');
|
|
41
|
+
if (!ServerlessInstance) {
|
|
42
|
+
throw new mcp_error_1.InternalMcpError('@frontmcp/sdk version mismatch, make sure you have the same version for all @frontmcp/* packages', 'SDK_VERSION_MISMATCH');
|
|
43
|
+
}
|
|
44
|
+
const handlerPromise = ServerlessInstance.createHandler(metadata);
|
|
45
|
+
setServerlessHandlerPromise(handlerPromise);
|
|
46
|
+
handlerPromise.then(setServerlessHandler).catch((err) => {
|
|
47
|
+
const e = err instanceof Error ? err : new mcp_error_1.InternalMcpError(String(err), 'SERVERLESS_INIT_FAILED');
|
|
48
|
+
setServerlessHandlerError(e);
|
|
49
|
+
console.error('[FrontMCP] Serverless initialization failed:', e);
|
|
53
50
|
});
|
|
54
51
|
}
|
|
55
52
|
else if (metadata.serve) {
|
|
@@ -57,7 +54,7 @@ function FrontMcp(providedMetadata) {
|
|
|
57
54
|
const sdk = '@frontmcp/sdk';
|
|
58
55
|
import(sdk).then(({ FrontMcpInstance }) => {
|
|
59
56
|
if (!FrontMcpInstance) {
|
|
60
|
-
throw new
|
|
57
|
+
throw new mcp_error_1.InternalMcpError(`${sdk} version mismatch, make sure you have the same version for all @frontmcp/* packages`, 'SDK_VERSION_MISMATCH');
|
|
61
58
|
}
|
|
62
59
|
FrontMcpInstance.bootstrap(metadata);
|
|
63
60
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"front-mcp.decorator.js","sourceRoot":"","sources":["../../../../src/common/decorators/front-mcp.decorator.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"front-mcp.decorator.js","sourceRoot":"","sources":["../../../../src/common/decorators/front-mcp.decorator.ts"],"names":[],"mappings":"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","sourcesContent":["import 'reflect-metadata';\nimport { FrontMcpTokens } from '../tokens';\nimport { FrontMcpMetadata, frontMcpMetadataSchema } from '../metadata';\nimport { FrontMcpInstance } from '../../front-mcp';\nimport { applyMigration } from '../migrate';\nimport { InternalMcpError } from '../../errors/mcp.error';\n\n/**\n * Decorator that marks a class as a FrontMcp Server and provides metadata\n */\nexport function FrontMcp(providedMetadata: FrontMcpMetadata): ClassDecorator {\n return (target: Function) => {\n // Apply migration for deprecated auth.transport and session configs\n const migratedMetadata = applyMigration(providedMetadata);\n const { error, data: metadata } = frontMcpMetadataSchema.safeParse(migratedMetadata);\n if (error) {\n if (error.format().apps) {\n throw new Error(\n `Invalid metadata provided to @FrontMcp { apps: [?] }: \\n${JSON.stringify(error.format().apps, null, 2)}`,\n );\n }\n if (error.format().providers) {\n throw new Error(\n `Invalid metadata provided to @FrontMcp { providers: [?] }: \\n${JSON.stringify(\n error.format().providers,\n null,\n 2,\n )}`,\n );\n }\n const loggingFormat = error.format()['logging'] as Record<string, unknown> | undefined;\n if (loggingFormat?.['transports']) {\n throw new Error(\n `Invalid metadata provided to @FrontMcp { logging: { transports: [?] } }: \\n${JSON.stringify(\n loggingFormat['transports'],\n null,\n 2,\n )}`,\n );\n }\n throw error;\n }\n\n Reflect.defineMetadata(FrontMcpTokens.type, true, target);\n for (const property in metadata) {\n Reflect.defineMetadata(FrontMcpTokens[property] ?? property, metadata[property], target);\n }\n\n // Safe check for serverless mode - process.env may not exist in Cloudflare Workers\n const isServerless = typeof process !== 'undefined' && process.env?.['FRONTMCP_SERVERLESS'] === '1';\n\n if (isServerless) {\n // Serverless mode: bootstrap, prepare (no listen), store handler globally\n // Use synchronous require for bundler compatibility (rspack/webpack)\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const {\n FrontMcpInstance: ServerlessInstance,\n setServerlessHandler,\n setServerlessHandlerPromise,\n setServerlessHandlerError,\n }: {\n FrontMcpInstance: typeof FrontMcpInstance;\n setServerlessHandler: (handler: unknown) => void;\n setServerlessHandlerPromise: (promise: Promise<unknown>) => void;\n setServerlessHandlerError: (error: Error) => void;\n } = require('@frontmcp/sdk');\n\n if (!ServerlessInstance) {\n throw new InternalMcpError(\n '@frontmcp/sdk version mismatch, make sure you have the same version for all @frontmcp/* packages',\n 'SDK_VERSION_MISMATCH',\n );\n }\n\n const handlerPromise = ServerlessInstance.createHandler(metadata);\n setServerlessHandlerPromise(handlerPromise);\n handlerPromise.then(setServerlessHandler).catch((err: unknown) => {\n const e = err instanceof Error ? err : new InternalMcpError(String(err), 'SERVERLESS_INIT_FAILED');\n setServerlessHandlerError(e);\n console.error('[FrontMCP] Serverless initialization failed:', e);\n });\n } else if (metadata.serve) {\n // Normal mode: bootstrap and start server\n const sdk = '@frontmcp/sdk';\n import(sdk).then(({ FrontMcpInstance }) => {\n if (!FrontMcpInstance) {\n throw new InternalMcpError(\n `${sdk} version mismatch, make sure you have the same version for all @frontmcp/* packages`,\n 'SDK_VERSION_MISMATCH',\n );\n }\n\n FrontMcpInstance.bootstrap(metadata);\n });\n }\n };\n}\n"]}
|