@forestadmin/mcp-server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -0
- package/dist/__mocks__/version.d.ts +3 -0
- package/dist/__mocks__/version.js +7 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +14 -0
- package/dist/factory.d.ts +51 -0
- package/dist/factory.js +40 -0
- package/dist/forest-oauth-provider.d.ts +44 -0
- package/dist/forest-oauth-provider.js +253 -0
- package/dist/forest-oauth-provider.test.d.ts +2 -0
- package/dist/forest-oauth-provider.test.js +590 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +13 -0
- package/dist/mcp-paths.d.ts +5 -0
- package/dist/mcp-paths.js +11 -0
- package/dist/polyfills.d.ts +12 -0
- package/dist/polyfills.js +27 -0
- package/dist/schemas/filter.d.ts +4 -0
- package/dist/schemas/filter.js +70 -0
- package/dist/schemas/filter.test.d.ts +2 -0
- package/dist/schemas/filter.test.js +234 -0
- package/dist/server.d.ts +87 -0
- package/dist/server.js +341 -0
- package/dist/server.test.d.ts +2 -0
- package/dist/server.test.js +901 -0
- package/dist/test-utils/mock-server.d.ts +62 -0
- package/dist/test-utils/mock-server.js +187 -0
- package/dist/tools/list.d.ts +4 -0
- package/dist/tools/list.js +98 -0
- package/dist/tools/list.test.d.ts +2 -0
- package/dist/tools/list.test.js +385 -0
- package/dist/utils/activity-logs-creator.d.ts +9 -0
- package/dist/utils/activity-logs-creator.js +65 -0
- package/dist/utils/activity-logs-creator.test.d.ts +2 -0
- package/dist/utils/activity-logs-creator.test.js +239 -0
- package/dist/utils/agent-caller.d.ts +13 -0
- package/dist/utils/agent-caller.js +24 -0
- package/dist/utils/agent-caller.test.d.ts +2 -0
- package/dist/utils/agent-caller.test.js +102 -0
- package/dist/utils/error-parser.d.ts +10 -0
- package/dist/utils/error-parser.js +56 -0
- package/dist/utils/error-parser.test.d.ts +2 -0
- package/dist/utils/error-parser.test.js +124 -0
- package/dist/utils/schema-fetcher.d.ts +53 -0
- package/dist/utils/schema-fetcher.js +85 -0
- package/dist/utils/schema-fetcher.test.d.ts +2 -0
- package/dist/utils/schema-fetcher.test.js +212 -0
- package/dist/utils/sse-error-logger.d.ts +14 -0
- package/dist/utils/sse-error-logger.js +112 -0
- package/dist/utils/tool-with-logging.d.ts +44 -0
- package/dist/utils/tool-with-logging.js +66 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.js +43 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# @forestadmin/mcp-server
|
|
2
|
+
|
|
3
|
+
Model Context Protocol (MCP) server for Forest Admin with OAuth authentication support.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This MCP server provides HTTP REST API access to Forest Admin operations, enabling AI assistants and other MCP clients to interact with your Forest Admin data through a standardized protocol.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
### With Forest Admin Agent
|
|
12
|
+
|
|
13
|
+
The MCP server is included with the Forest Admin agent. Simply call `mountAiMcpServer()`:
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { createAgent } from '@forestadmin/agent';
|
|
17
|
+
|
|
18
|
+
const agent = createAgent(options)
|
|
19
|
+
.addDataSource(myDataSource)
|
|
20
|
+
.mountAiMcpServer();
|
|
21
|
+
|
|
22
|
+
agent.mountOnExpress(app);
|
|
23
|
+
agent.start();
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The MCP server will be automatically initialized and mounted on your application.
|
|
27
|
+
|
|
28
|
+
### Standalone Server
|
|
29
|
+
|
|
30
|
+
You can also run the MCP server standalone using the CLI:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx forest-mcp-server
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Or programmatically:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
node dist/index.js
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Environment Variables
|
|
43
|
+
|
|
44
|
+
The following environment variables are required to run the server:
|
|
45
|
+
|
|
46
|
+
| Variable | Required | Default | Description |
|
|
47
|
+
|----------|----------|---------|-------------|
|
|
48
|
+
| `FOREST_ENV_SECRET` | **Yes** | - | Your Forest Admin environment secret |
|
|
49
|
+
| `FOREST_AUTH_SECRET` | **Yes** | - | Your Forest Admin authentication secret (must match your agent) |
|
|
50
|
+
| `MCP_SERVER_PORT` | No | `3931` | Port for the HTTP server |
|
|
51
|
+
|
|
52
|
+
### Example Configuration
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
export FOREST_ENV_SECRET="your-env-secret"
|
|
56
|
+
export FOREST_AUTH_SECRET="your-auth-secret"
|
|
57
|
+
export MCP_SERVER_PORT=3931
|
|
58
|
+
|
|
59
|
+
npx forest-mcp-server
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## API Endpoint
|
|
63
|
+
|
|
64
|
+
Once running, the MCP server exposes a single endpoint:
|
|
65
|
+
|
|
66
|
+
- **POST** `/mcp` - Main MCP protocol endpoint
|
|
67
|
+
|
|
68
|
+
The server expects MCP protocol messages in the request body and returns MCP-formatted responses.
|
|
69
|
+
|
|
70
|
+
## Features
|
|
71
|
+
|
|
72
|
+
- **HTTP Transport**: Uses streamable HTTP transport for MCP communication
|
|
73
|
+
- **OAuth Authentication**: Built-in support for Forest Admin OAuth
|
|
74
|
+
- **CORS Enabled**: Allows cross-origin requests
|
|
75
|
+
- **Express-based**: Built on top of Express.js for reliability and extensibility
|
|
76
|
+
|
|
77
|
+
## Development
|
|
78
|
+
|
|
79
|
+
### Building
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npm run build
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Watch Mode
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npm run build:watch
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Linting
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npm run lint
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Testing
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npm test
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Cleaning
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npm run clean
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Architecture
|
|
110
|
+
|
|
111
|
+
The server consists of:
|
|
112
|
+
|
|
113
|
+
- **ForestMCPServer**: Main server class managing the MCP server lifecycle
|
|
114
|
+
- **McpServer**: Core MCP protocol implementation
|
|
115
|
+
- **StreamableHTTPServerTransport**: HTTP transport layer for MCP
|
|
116
|
+
- **Express App**: HTTP server handling incoming requests
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
GPL-3.0
|
|
121
|
+
|
|
122
|
+
## Repository
|
|
123
|
+
|
|
124
|
+
[https://github.com/ForestAdmin/agent-nodejs](https://github.com/ForestAdmin/agent-nodejs)
|
|
125
|
+
|
|
126
|
+
## Support
|
|
127
|
+
|
|
128
|
+
For issues and feature requests, please visit the [GitHub repository](https://github.com/ForestAdmin/agent-nodejs/tree/main/packages/mcp-server).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NAME = exports.VERSION = void 0;
|
|
4
|
+
// Mock version module for Jest (avoids import.meta.url issues in CommonJS)
|
|
5
|
+
exports.VERSION = '0.1.0';
|
|
6
|
+
exports.NAME = '@forestadmin/mcp-server';
|
|
7
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmVyc2lvbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9fX21vY2tzX18vdmVyc2lvbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSwyRUFBMkU7QUFDOUQsUUFBQSxPQUFPLEdBQUcsT0FBTyxDQUFDO0FBQ2xCLFFBQUEsSUFBSSxHQUFHLHlCQUF5QixDQUFDIn0=
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const server_1 = __importDefault(require("./server"));
|
|
8
|
+
// Start the server when run directly as CLI
|
|
9
|
+
const server = new server_1.default();
|
|
10
|
+
server.run().catch(error => {
|
|
11
|
+
console.error('[FATAL] Server crashed:', error);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
});
|
|
14
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xpLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL2NsaS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7QUFFQSxzREFBdUM7QUFFdkMsNENBQTRDO0FBQzVDLE1BQU0sTUFBTSxHQUFHLElBQUksZ0JBQWUsRUFBRSxDQUFDO0FBRXJDLE1BQU0sQ0FBQyxHQUFHLEVBQUUsQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDLEVBQUU7SUFDekIsT0FBTyxDQUFDLEtBQUssQ0FBQyx5QkFBeUIsRUFBRSxLQUFLLENBQUMsQ0FBQztJQUNoRCxPQUFPLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBQ2xCLENBQUMsQ0FBQyxDQUFDIn0=
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { HttpCallback } from './server';
|
|
2
|
+
import { Logger } from './server';
|
|
3
|
+
/**
|
|
4
|
+
* Context passed from the Forest Admin agent to the MCP factory.
|
|
5
|
+
*/
|
|
6
|
+
export interface McpFactoryContext {
|
|
7
|
+
/** Forest Admin server URL */
|
|
8
|
+
forestServerUrl: string;
|
|
9
|
+
/** Environment secret */
|
|
10
|
+
envSecret: string;
|
|
11
|
+
/** Authentication secret */
|
|
12
|
+
authSecret: string;
|
|
13
|
+
/** Logger function */
|
|
14
|
+
logger: Logger;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Options for the MCP factory function.
|
|
18
|
+
*/
|
|
19
|
+
export interface McpFactoryOptions {
|
|
20
|
+
/**
|
|
21
|
+
* Optional override for the base URL where the agent is publicly accessible.
|
|
22
|
+
* If not provided, it will be automatically fetched from Forest Admin API
|
|
23
|
+
* (the environment's api_endpoint configuration).
|
|
24
|
+
* Example: 'https://my-app.example.com' or 'http://localhost:3000'
|
|
25
|
+
*/
|
|
26
|
+
baseUrl?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Factory function to create an MCP HTTP callback for use with the Forest Admin agent.
|
|
30
|
+
*
|
|
31
|
+
* This function is designed to be used with the `agent.useMcp()` method:
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* import { createAgent } from '@forestadmin/agent';
|
|
36
|
+
* import { createMcpServer } from '@forestadmin/mcp-server';
|
|
37
|
+
*
|
|
38
|
+
* const agent = createAgent(options)
|
|
39
|
+
* .addDataSource(myDataSource)
|
|
40
|
+
* .useMcp(createMcpServer, { baseUrl: 'https://my-app.example.com' });
|
|
41
|
+
*
|
|
42
|
+
* agent.mountOnExpress(app);
|
|
43
|
+
* agent.start();
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* @param context - Context containing Forest Admin configuration (provided by the agent)
|
|
47
|
+
* @param options - Optional configuration for the MCP server
|
|
48
|
+
* @returns An HTTP callback that handles MCP routes
|
|
49
|
+
*/
|
|
50
|
+
export declare function createMcpServer(context: McpFactoryContext, options?: McpFactoryOptions): Promise<HttpCallback>;
|
|
51
|
+
//# sourceMappingURL=factory.d.ts.map
|
package/dist/factory.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createMcpServer = createMcpServer;
|
|
7
|
+
const server_1 = __importDefault(require("./server"));
|
|
8
|
+
/**
|
|
9
|
+
* Factory function to create an MCP HTTP callback for use with the Forest Admin agent.
|
|
10
|
+
*
|
|
11
|
+
* This function is designed to be used with the `agent.useMcp()` method:
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { createAgent } from '@forestadmin/agent';
|
|
16
|
+
* import { createMcpServer } from '@forestadmin/mcp-server';
|
|
17
|
+
*
|
|
18
|
+
* const agent = createAgent(options)
|
|
19
|
+
* .addDataSource(myDataSource)
|
|
20
|
+
* .useMcp(createMcpServer, { baseUrl: 'https://my-app.example.com' });
|
|
21
|
+
*
|
|
22
|
+
* agent.mountOnExpress(app);
|
|
23
|
+
* agent.start();
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @param context - Context containing Forest Admin configuration (provided by the agent)
|
|
27
|
+
* @param options - Optional configuration for the MCP server
|
|
28
|
+
* @returns An HTTP callback that handles MCP routes
|
|
29
|
+
*/
|
|
30
|
+
async function createMcpServer(context, options) {
|
|
31
|
+
const mcpServer = new server_1.default({
|
|
32
|
+
forestServerUrl: context.forestServerUrl,
|
|
33
|
+
envSecret: context.envSecret,
|
|
34
|
+
authSecret: context.authSecret,
|
|
35
|
+
logger: context.logger,
|
|
36
|
+
});
|
|
37
|
+
const baseUrl = options?.baseUrl ? new URL('/', options.baseUrl) : undefined;
|
|
38
|
+
return mcpServer.getHttpCallback(baseUrl);
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZmFjdG9yeS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9mYWN0b3J5LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7O0FBcURBLDBDQWNDO0FBakVELHNEQUFtRDtBQTZCbkQ7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztHQXFCRztBQUNJLEtBQUssVUFBVSxlQUFlLENBQ25DLE9BQTBCLEVBQzFCLE9BQTJCO0lBRTNCLE1BQU0sU0FBUyxHQUFHLElBQUksZ0JBQWUsQ0FBQztRQUNwQyxlQUFlLEVBQUUsT0FBTyxDQUFDLGVBQWU7UUFDeEMsU0FBUyxFQUFFLE9BQU8sQ0FBQyxTQUFTO1FBQzVCLFVBQVUsRUFBRSxPQUFPLENBQUMsVUFBVTtRQUM5QixNQUFNLEVBQUUsT0FBTyxDQUFDLE1BQU07S0FDdkIsQ0FBQyxDQUFDO0lBRUgsTUFBTSxPQUFPLEdBQUcsT0FBTyxFQUFFLE9BQU8sQ0FBQyxDQUFDLENBQUMsSUFBSSxHQUFHLENBQUMsR0FBRyxFQUFFLE9BQU8sQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDO0lBRTdFLE9BQU8sU0FBUyxDQUFDLGVBQWUsQ0FBQyxPQUFPLENBQUMsQ0FBQztBQUM1QyxDQUFDIn0=
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Logger } from './server';
|
|
2
|
+
import type { OAuthRegisteredClientsStore } from '@modelcontextprotocol/sdk/server/auth/clients.js';
|
|
3
|
+
import type { AuthorizationParams, OAuthServerProvider } from '@modelcontextprotocol/sdk/server/auth/provider.js';
|
|
4
|
+
import type { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
|
|
5
|
+
import type { OAuthClientInformationFull, OAuthTokenRevocationRequest, OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth.js';
|
|
6
|
+
import type { Response } from 'express';
|
|
7
|
+
export interface ForestOAuthProviderOptions {
|
|
8
|
+
forestServerUrl: string;
|
|
9
|
+
forestAppUrl: string;
|
|
10
|
+
envSecret: string;
|
|
11
|
+
authSecret: string;
|
|
12
|
+
logger: Logger;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* OAuth Server Provider that integrates with Forest Admin authentication
|
|
16
|
+
*/
|
|
17
|
+
export default class ForestOAuthProvider implements OAuthServerProvider {
|
|
18
|
+
private forestServerUrl;
|
|
19
|
+
private forestAppUrl;
|
|
20
|
+
private envSecret;
|
|
21
|
+
private authSecret;
|
|
22
|
+
private forestClient;
|
|
23
|
+
private environmentId?;
|
|
24
|
+
private environmentApiEndpoint?;
|
|
25
|
+
private logger;
|
|
26
|
+
constructor({ forestServerUrl, forestAppUrl, envSecret, authSecret, logger, }: ForestOAuthProviderOptions);
|
|
27
|
+
initialize(): Promise<void>;
|
|
28
|
+
private fetchEnvironmentId;
|
|
29
|
+
/**
|
|
30
|
+
* Get the base URL for the MCP server from the environment's api_endpoint.
|
|
31
|
+
* Returns undefined if the environment info hasn't been fetched yet.
|
|
32
|
+
*/
|
|
33
|
+
getBaseUrl(): URL | undefined;
|
|
34
|
+
get clientsStore(): OAuthRegisteredClientsStore;
|
|
35
|
+
authorize(client: OAuthClientInformationFull, params: AuthorizationParams, res: Response): Promise<void>;
|
|
36
|
+
challengeForAuthorizationCode(client: OAuthClientInformationFull, authorizationCode: string): Promise<string>;
|
|
37
|
+
exchangeAuthorizationCode(client: OAuthClientInformationFull, authorizationCode: string, codeVerifier?: string, redirectUri?: string): Promise<OAuthTokens>;
|
|
38
|
+
exchangeRefreshToken(client: OAuthClientInformationFull, refreshToken: string, scopes?: string[]): Promise<OAuthTokens>;
|
|
39
|
+
private generateAccessToken;
|
|
40
|
+
verifyAccessToken(token: string): Promise<AuthInfo>;
|
|
41
|
+
revokeToken(_client: OAuthClientInformationFull, _request: OAuthTokenRevocationRequest): Promise<void>;
|
|
42
|
+
skipLocalPkceValidation: boolean;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=forest-oauth-provider.d.ts.map
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const forestadmin_client_1 = __importDefault(require("@forestadmin/forestadmin-client"));
|
|
7
|
+
const errors_js_1 = require("@modelcontextprotocol/sdk/server/auth/errors.js");
|
|
8
|
+
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
9
|
+
/**
|
|
10
|
+
* OAuth Server Provider that integrates with Forest Admin authentication
|
|
11
|
+
*/
|
|
12
|
+
class ForestOAuthProvider {
|
|
13
|
+
constructor({ forestServerUrl, forestAppUrl, envSecret, authSecret, logger, }) {
|
|
14
|
+
// Skip PKCE validation to match original implementation
|
|
15
|
+
this.skipLocalPkceValidation = true;
|
|
16
|
+
this.forestServerUrl = forestServerUrl;
|
|
17
|
+
this.forestAppUrl = forestAppUrl;
|
|
18
|
+
this.envSecret = envSecret;
|
|
19
|
+
this.authSecret = authSecret;
|
|
20
|
+
this.logger = logger;
|
|
21
|
+
this.forestClient = (0, forestadmin_client_1.default)({
|
|
22
|
+
forestServerUrl: this.forestServerUrl,
|
|
23
|
+
envSecret: this.envSecret,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
async initialize() {
|
|
27
|
+
try {
|
|
28
|
+
await this.fetchEnvironmentId();
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
// Log warning but don't throw - the MCP server can still partially function
|
|
32
|
+
// The authorize method will return an appropriate error when environmentId is missing
|
|
33
|
+
this.logger('Warn', `Failed to fetch environmentId from Forest Admin API: ${error}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async fetchEnvironmentId() {
|
|
37
|
+
if (!this.envSecret) {
|
|
38
|
+
throw new Error('FOREST_ENV_SECRET is required to fetch environment ID');
|
|
39
|
+
}
|
|
40
|
+
// Call Forest Admin API to get environment information
|
|
41
|
+
const response = await fetch(`${this.forestServerUrl}/liana/environment`, {
|
|
42
|
+
method: 'GET',
|
|
43
|
+
headers: {
|
|
44
|
+
'forest-secret-key': this.envSecret,
|
|
45
|
+
'Content-Type': 'application/json',
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
const errorText = await response.text();
|
|
50
|
+
throw new Error(`Failed to fetch environment from Forest Admin API: ${response.status} ${response.statusText}. ${errorText}`);
|
|
51
|
+
}
|
|
52
|
+
const data = (await response.json());
|
|
53
|
+
this.environmentId = parseInt(data.data.id, 10);
|
|
54
|
+
this.environmentApiEndpoint = data.data.attributes.api_endpoint;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Get the base URL for the MCP server from the environment's api_endpoint.
|
|
58
|
+
* Returns undefined if the environment info hasn't been fetched yet.
|
|
59
|
+
*/
|
|
60
|
+
getBaseUrl() {
|
|
61
|
+
if (!this.environmentApiEndpoint) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
return new URL(this.environmentApiEndpoint);
|
|
65
|
+
}
|
|
66
|
+
get clientsStore() {
|
|
67
|
+
return {
|
|
68
|
+
getClient: async (clientId) => {
|
|
69
|
+
// Call Forest Admin API to get client information
|
|
70
|
+
const response = await fetch(`${this.forestServerUrl}/oauth/register/${clientId}`, {
|
|
71
|
+
method: 'GET',
|
|
72
|
+
headers: {
|
|
73
|
+
'Content-Type': 'application/json',
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
// Log and return undefined for other errors (don't expose internal errors)
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
console.error(`[ForestOAuthProvider] Failed to fetch client ${clientId}: ${response.status} ${response.statusText}`);
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
// Return registered client if exists
|
|
82
|
+
return response.json();
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async authorize(client, params, res) {
|
|
87
|
+
try {
|
|
88
|
+
// Ensure environmentId is available
|
|
89
|
+
if (!this.environmentId) {
|
|
90
|
+
throw new Error('Environment ID not available. Make sure initialize() was called and the Forest Admin API is reachable.');
|
|
91
|
+
}
|
|
92
|
+
// Redirect to Forest Admin agent for actual authentication
|
|
93
|
+
const agentAuthUrl = new URL('/oauth/authorize', this.forestAppUrl);
|
|
94
|
+
agentAuthUrl.searchParams.set('redirect_uri', params.redirectUri);
|
|
95
|
+
agentAuthUrl.searchParams.set('code_challenge', params.codeChallenge);
|
|
96
|
+
agentAuthUrl.searchParams.set('code_challenge_method', 'S256');
|
|
97
|
+
agentAuthUrl.searchParams.set('response_type', 'code');
|
|
98
|
+
agentAuthUrl.searchParams.set('client_id', client.client_id);
|
|
99
|
+
agentAuthUrl.searchParams.set('state', params.state);
|
|
100
|
+
agentAuthUrl.searchParams.set('scope', params.scopes.join('+'));
|
|
101
|
+
if (params.resource?.href) {
|
|
102
|
+
agentAuthUrl.searchParams.set('resource', params.resource.href);
|
|
103
|
+
}
|
|
104
|
+
agentAuthUrl.searchParams.set('environmentId', this.environmentId.toString());
|
|
105
|
+
res.redirect(agentAuthUrl.toString());
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
109
|
+
this.logger('Error', `[ForestOAuthProvider] Authorization error:: ${errorMessage}`);
|
|
110
|
+
// Don't expose internal error details to the client - use a generic message
|
|
111
|
+
// The actual error is logged above for debugging
|
|
112
|
+
res.redirect(`${params.redirectUri}?error=server_error&error_description=${encodeURIComponent('Authorization failed. Please try again or contact support.')}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async challengeForAuthorizationCode(client, authorizationCode) {
|
|
116
|
+
// This is never called but required by TS !
|
|
117
|
+
return authorizationCode;
|
|
118
|
+
}
|
|
119
|
+
async exchangeAuthorizationCode(client, authorizationCode, codeVerifier, redirectUri) {
|
|
120
|
+
try {
|
|
121
|
+
return await this.generateAccessToken(client, {
|
|
122
|
+
grant_type: 'authorization_code',
|
|
123
|
+
code: authorizationCode,
|
|
124
|
+
redirect_uri: redirectUri,
|
|
125
|
+
client_id: client.client_id,
|
|
126
|
+
code_verifier: codeVerifier,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
131
|
+
throw new errors_js_1.InvalidRequestError(`Failed to exchange authorization code: ${message}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async exchangeRefreshToken(client, refreshToken, scopes) {
|
|
135
|
+
// Verify and decode the refresh token
|
|
136
|
+
let decoded;
|
|
137
|
+
try {
|
|
138
|
+
decoded = jsonwebtoken_1.default.verify(refreshToken, this.authSecret);
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
throw new errors_js_1.InvalidTokenError('Invalid or expired refresh token');
|
|
142
|
+
}
|
|
143
|
+
// Validate token type
|
|
144
|
+
if (decoded.type !== 'refresh') {
|
|
145
|
+
throw new errors_js_1.UnsupportedTokenTypeError('Invalid token type');
|
|
146
|
+
}
|
|
147
|
+
// Validate client_id matches
|
|
148
|
+
if (decoded.clientId !== client.client_id) {
|
|
149
|
+
throw new errors_js_1.InvalidClientError('Token was not issued to this client');
|
|
150
|
+
}
|
|
151
|
+
// Exchange the Forest refresh token for new tokens
|
|
152
|
+
try {
|
|
153
|
+
return await this.generateAccessToken(client, {
|
|
154
|
+
grant_type: 'refresh_token',
|
|
155
|
+
refresh_token: decoded.serverRefreshToken,
|
|
156
|
+
client_id: client.client_id,
|
|
157
|
+
scopes,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
162
|
+
throw new errors_js_1.InvalidRequestError(`Failed to refresh token: ${message}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async generateAccessToken(client, tokenPayload) {
|
|
166
|
+
const response = await fetch(`${this.forestServerUrl}/oauth/token`, {
|
|
167
|
+
method: 'POST',
|
|
168
|
+
headers: {
|
|
169
|
+
'forest-secret-key': this.envSecret,
|
|
170
|
+
'Content-Type': 'application/json',
|
|
171
|
+
},
|
|
172
|
+
body: JSON.stringify(tokenPayload),
|
|
173
|
+
});
|
|
174
|
+
if (!response.ok) {
|
|
175
|
+
const errorBody = await response.json();
|
|
176
|
+
throw new errors_js_1.CustomOAuthError(errorBody.error || 'server_error', errorBody.error_description || 'Failed to exchange authorization code');
|
|
177
|
+
}
|
|
178
|
+
const { access_token: forestServerAccessToken, refresh_token: forestServerRefreshToken } = (await response.json());
|
|
179
|
+
// Get updated user info
|
|
180
|
+
const decodedAccessToken = jsonwebtoken_1.default.decode(forestServerAccessToken);
|
|
181
|
+
if (!decodedAccessToken) {
|
|
182
|
+
throw new Error('Failed to decode access token from Forest Admin server');
|
|
183
|
+
}
|
|
184
|
+
const { meta: { renderingId }, exp: expirationDate, scope, } = decodedAccessToken;
|
|
185
|
+
const decodedRefreshToken = jsonwebtoken_1.default.decode(forestServerRefreshToken);
|
|
186
|
+
if (!decodedRefreshToken) {
|
|
187
|
+
throw new Error('Failed to decode refresh token from Forest Admin server');
|
|
188
|
+
}
|
|
189
|
+
const { exp: refreshTokenExpirationDate } = decodedRefreshToken;
|
|
190
|
+
const user = await this.forestClient.authService.getUserInfo(renderingId, forestServerAccessToken);
|
|
191
|
+
// Create new access token
|
|
192
|
+
const expiresIn = expirationDate - Math.floor(Date.now() / 1000);
|
|
193
|
+
const tokenScopes = scope ? scope.split(' ') : ['mcp:read', 'mcp:write', 'mcp:action'];
|
|
194
|
+
const accessToken = jsonwebtoken_1.default.sign({ ...user, serverToken: forestServerAccessToken, scopes: tokenScopes }, this.authSecret, { expiresIn });
|
|
195
|
+
// Create new refresh token (token rotation for security)
|
|
196
|
+
const refreshToken = jsonwebtoken_1.default.sign({
|
|
197
|
+
type: 'refresh',
|
|
198
|
+
clientId: client.client_id,
|
|
199
|
+
userId: user.id,
|
|
200
|
+
renderingId,
|
|
201
|
+
serverRefreshToken: forestServerRefreshToken,
|
|
202
|
+
}, this.authSecret, { expiresIn: refreshTokenExpirationDate - Math.floor(Date.now() / 1000) });
|
|
203
|
+
return {
|
|
204
|
+
access_token: accessToken,
|
|
205
|
+
token_type: 'Bearer',
|
|
206
|
+
expires_in: expiresIn > 0 ? expiresIn : 3600,
|
|
207
|
+
refresh_token: refreshToken,
|
|
208
|
+
scope: scope || client.scope,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
async verifyAccessToken(token) {
|
|
212
|
+
try {
|
|
213
|
+
const decoded = jsonwebtoken_1.default.verify(token, this.authSecret);
|
|
214
|
+
// Ensure this is an access token (not a refresh token)
|
|
215
|
+
if ('type' in decoded && decoded.type === 'refresh') {
|
|
216
|
+
throw new errors_js_1.UnsupportedTokenTypeError('Cannot use refresh token as access token');
|
|
217
|
+
}
|
|
218
|
+
// Use scopes from token if available, otherwise fall back to defaults
|
|
219
|
+
const scopes = decoded.scopes || ['mcp:read', 'mcp:write', 'mcp:action'];
|
|
220
|
+
return {
|
|
221
|
+
token,
|
|
222
|
+
clientId: decoded.id.toString(),
|
|
223
|
+
expiresAt: decoded.exp,
|
|
224
|
+
scopes,
|
|
225
|
+
extra: {
|
|
226
|
+
userId: decoded.id,
|
|
227
|
+
email: decoded.email,
|
|
228
|
+
renderingId: decoded.renderingId,
|
|
229
|
+
environmentApiEndpoint: this.environmentApiEndpoint,
|
|
230
|
+
forestServerToken: decoded.serverToken,
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
this.logger('Error', `Error verifying token: ${error}`);
|
|
236
|
+
if (error instanceof jsonwebtoken_1.default.TokenExpiredError) {
|
|
237
|
+
throw new errors_js_1.InvalidTokenError('Access token has expired');
|
|
238
|
+
}
|
|
239
|
+
if (error instanceof jsonwebtoken_1.default.JsonWebTokenError) {
|
|
240
|
+
throw new errors_js_1.InvalidTokenError('Invalid access token');
|
|
241
|
+
}
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
async revokeToken(_client, _request) {
|
|
246
|
+
// Token revocation is not currently implemented.
|
|
247
|
+
// Per RFC 7009, the revocation endpoint should return success even if the token
|
|
248
|
+
// is already invalid or unknown, so we silently succeed here.
|
|
249
|
+
// TODO: Implement actual token revocation with Forest Admin server when supported.
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
exports.default = ForestOAuthProvider;
|
|
253
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZm9yZXN0LW9hdXRoLXByb3ZpZGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL2ZvcmVzdC1vYXV0aC1wcm92aWRlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7OztBQWVBLHlGQUFzRTtBQUN0RSwrRUFNeUQ7QUFDekQsZ0VBQXdDO0FBVXhDOztHQUVHO0FBQ0gsTUFBcUIsbUJBQW1CO0lBVXRDLFlBQVksRUFDVixlQUFlLEVBQ2YsWUFBWSxFQUNaLFNBQVMsRUFDVCxVQUFVLEVBQ1YsTUFBTSxHQUNxQjtRQXNXN0Isd0RBQXdEO1FBQ3hELDRCQUF1QixHQUFHLElBQUksQ0FBQztRQXRXN0IsSUFBSSxDQUFDLGVBQWUsR0FBRyxlQUFlLENBQUM7UUFDdkMsSUFBSSxDQUFDLFlBQVksR0FBRyxZQUFZLENBQUM7UUFDakMsSUFBSSxDQUFDLFNBQVMsR0FBRyxTQUFTLENBQUM7UUFDM0IsSUFBSSxDQUFDLFVBQVUsR0FBRyxVQUFVLENBQUM7UUFDN0IsSUFBSSxDQUFDLE1BQU0sR0FBRyxNQUFNLENBQUM7UUFDckIsSUFBSSxDQUFDLFlBQVksR0FBRyxJQUFBLDRCQUF1QixFQUFDO1lBQzFDLGVBQWUsRUFBRSxJQUFJLENBQUMsZUFBZTtZQUNyQyxTQUFTLEVBQUUsSUFBSSxDQUFDLFNBQVM7U0FDMUIsQ0FBQyxDQUFDO0lBQ0wsQ0FBQztJQUVELEtBQUssQ0FBQyxVQUFVO1FBQ2QsSUFBSSxDQUFDO1lBQ0gsTUFBTSxJQUFJLENBQUMsa0JBQWtCLEVBQUUsQ0FBQztRQUNsQyxDQUFDO1FBQUMsT0FBTyxLQUFLLEVBQUUsQ0FBQztZQUNmLDRFQUE0RTtZQUM1RSxzRkFBc0Y7WUFDdEYsSUFBSSxDQUFDLE1BQU0sQ0FBQyxNQUFNLEVBQUUsd0RBQXdELEtBQUssRUFBRSxDQUFDLENBQUM7UUFDdkYsQ0FBQztJQUNILENBQUM7SUFFTyxLQUFLLENBQUMsa0JBQWtCO1FBQzlCLElBQUksQ0FBQyxJQUFJLENBQUMsU0FBUyxFQUFF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
|