@optimizely-opal/opal-tools-sdk 0.1.5-dev → 0.1.8-dev
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/.prettierignore +5 -0
- package/.prettierrc +1 -0
- package/README.md +20 -10
- package/dist/auth.d.ts +5 -5
- package/dist/auth.js +2 -2
- package/dist/decorators.d.ts +10 -8
- package/dist/decorators.js +5 -44
- package/dist/index.d.ts +10 -5
- package/dist/index.js +11 -5
- package/dist/models.d.ts +137 -116
- package/dist/models.js +104 -77
- package/dist/proteus.d.ts +1451 -0
- package/dist/proteus.js +86 -0
- package/dist/registerResource.d.ts +60 -0
- package/dist/registerResource.js +59 -0
- package/dist/registerTool.d.ts +79 -0
- package/dist/registerTool.js +106 -0
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +1 -1
- package/dist/service.d.ts +20 -6
- package/dist/service.js +106 -19
- package/eslint.config.js +20 -0
- package/package.json +20 -10
- package/scripts/generate-proteus.ts +135 -0
- package/scripts/lint.sh +7 -0
- package/src/auth.ts +21 -16
- package/src/decorators.ts +32 -67
- package/src/index.ts +10 -5
- package/src/models.ts +133 -103
- package/src/proteus.ts +2129 -0
- package/src/registerResource.ts +82 -0
- package/src/registerTool.ts +171 -0
- package/src/registry.ts +2 -2
- package/src/service.ts +161 -31
- package/tests/integration.test.ts +497 -0
- package/tests/proteus.test.ts +122 -0
- package/tsconfig.build.json +5 -0
- package/tsconfig.json +3 -3
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { ProteusDocument } from "./proteus";
|
|
2
|
+
import { registry } from "./registry";
|
|
3
|
+
|
|
4
|
+
type ResourceOptions = {
|
|
5
|
+
/** Description of the resource */
|
|
6
|
+
description?: string;
|
|
7
|
+
/** MIME type of the resource content (e.g., "application/vnd.opal.proteus+json") */
|
|
8
|
+
mimeType?: string;
|
|
9
|
+
/** Human-readable title for the resource */
|
|
10
|
+
title?: string;
|
|
11
|
+
/** The unique URI for this resource (e.g., "ui://my-app/create-form") */
|
|
12
|
+
uri: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Register a function as an MCP resource
|
|
17
|
+
*
|
|
18
|
+
* The handler can return either a string or a UI.Document. When returning a UI.Document,
|
|
19
|
+
* the SDK will automatically serialize it to JSON and set the MIME type to
|
|
20
|
+
* "application/vnd.opal.proteus+json" (if not already specified).
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* // Example 1: Returning a string (manual serialization)
|
|
25
|
+
* const getFormResource = registerResource('create-form', {
|
|
26
|
+
* uri: 'ui://my-app/create-form',
|
|
27
|
+
* description: 'Create form UI specification',
|
|
28
|
+
* mimeType: 'application/vnd.opal.proteus+json',
|
|
29
|
+
* title: 'Create Form'
|
|
30
|
+
* }, async () => {
|
|
31
|
+
* return JSON.stringify(proteusSpec);
|
|
32
|
+
* });
|
|
33
|
+
*
|
|
34
|
+
* // Example 2: Returning a UI.Document (automatic serialization)
|
|
35
|
+
* import { UI } from '@optimizely-opal/opal-tools-sdk';
|
|
36
|
+
*
|
|
37
|
+
* const getDynamicForm = registerResource('create-form', {
|
|
38
|
+
* uri: 'ui://my-app/create-form',
|
|
39
|
+
* description: 'Create form UI specification',
|
|
40
|
+
* title: 'Create Form'
|
|
41
|
+
* }, async () => {
|
|
42
|
+
* return UI.Document({
|
|
43
|
+
* appName: 'Item Manager',
|
|
44
|
+
* title: 'Create New Item',
|
|
45
|
+
* body: [
|
|
46
|
+
* UI.Heading({ children: 'Create Item' }),
|
|
47
|
+
* UI.Field({
|
|
48
|
+
* label: 'Name',
|
|
49
|
+
* children: UI.Input({ name: 'item_name' })
|
|
50
|
+
* })
|
|
51
|
+
* ],
|
|
52
|
+
* actions: [UI.Action({ children: 'Save' })]
|
|
53
|
+
* });
|
|
54
|
+
* });
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* @param name - Name of the resource
|
|
58
|
+
* @param options - Resource options (uri, description, mimeType, title)
|
|
59
|
+
* @param handler - Async function that returns the resource content (string or UI.Document)
|
|
60
|
+
* @returns The handler function (for convenience)
|
|
61
|
+
*/
|
|
62
|
+
export function registerResource(
|
|
63
|
+
name: string,
|
|
64
|
+
options: ResourceOptions,
|
|
65
|
+
handler: () => Promise<ProteusDocument | string> | ProteusDocument | string,
|
|
66
|
+
): typeof handler {
|
|
67
|
+
console.log(`Registering resource ${name} with URI ${options.uri}`);
|
|
68
|
+
|
|
69
|
+
// Register the resource with all services
|
|
70
|
+
for (const service of registry.services) {
|
|
71
|
+
service.registerResource(
|
|
72
|
+
options.uri,
|
|
73
|
+
name,
|
|
74
|
+
options.description,
|
|
75
|
+
options.mimeType,
|
|
76
|
+
options.title,
|
|
77
|
+
handler,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return handler;
|
|
82
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AuthRequirement,
|
|
5
|
+
Credentials,
|
|
6
|
+
Parameter,
|
|
7
|
+
ParameterType,
|
|
8
|
+
} from "./models";
|
|
9
|
+
import { registry } from "./registry";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Extra context passed to tool handlers
|
|
13
|
+
*/
|
|
14
|
+
export type RequestHandlerExtra = {
|
|
15
|
+
/** Authentication data if provided */
|
|
16
|
+
auth?: {
|
|
17
|
+
credentials: Credentials;
|
|
18
|
+
provider: string;
|
|
19
|
+
};
|
|
20
|
+
/** Execution mode: 'headless' for non-interactive, 'interactive' for user interaction (defaults to 'headless') */
|
|
21
|
+
mode: "headless" | "interactive";
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type ToolOptions<TSchema extends Record<string, z.ZodTypeAny>> = {
|
|
25
|
+
authRequirements?: {
|
|
26
|
+
provider: string;
|
|
27
|
+
required?: boolean;
|
|
28
|
+
scopeBundle: string;
|
|
29
|
+
};
|
|
30
|
+
description: string;
|
|
31
|
+
inputSchema: TSchema;
|
|
32
|
+
uiResource?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Register a function as an Opal tool
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```typescript
|
|
40
|
+
* const greetTool = registerTool('greet', {
|
|
41
|
+
* description: 'Greet a user',
|
|
42
|
+
* inputSchema: {
|
|
43
|
+
* name: z.string().describe('The name to greet')
|
|
44
|
+
* }
|
|
45
|
+
* }, async (params) => {
|
|
46
|
+
* // params is automatically typed as { name: string }
|
|
47
|
+
* return `Hello, ${params.name}!`;
|
|
48
|
+
* });
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* // With auth and execution mode
|
|
54
|
+
* const fetchTool = registerTool('fetch_data', {
|
|
55
|
+
* description: 'Fetch data from API',
|
|
56
|
+
* inputSchema: {
|
|
57
|
+
* url: z.string().describe('URL to fetch')
|
|
58
|
+
* },
|
|
59
|
+
* authRequirements: {
|
|
60
|
+
* provider: 'api-service',
|
|
61
|
+
* scopeBundle: 'read'
|
|
62
|
+
* }
|
|
63
|
+
* }, async (params, extra) => {
|
|
64
|
+
* // extra.mode: 'headless' | 'interactive'
|
|
65
|
+
* // extra.auth: { provider, credentials }
|
|
66
|
+
* const headers = extra?.auth ? { Authorization: extra.auth.credentials.token } : {};
|
|
67
|
+
* return fetch(params.url, { headers });
|
|
68
|
+
* });
|
|
69
|
+
* ```
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```typescript
|
|
73
|
+
* // With UI resource for dynamic rendering
|
|
74
|
+
* const createTaskTool = registerTool('create_task', {
|
|
75
|
+
* description: 'Create a new task',
|
|
76
|
+
* inputSchema: {
|
|
77
|
+
* title: z.string().describe('Task title'),
|
|
78
|
+
* description: z.string().describe('Task description')
|
|
79
|
+
* },
|
|
80
|
+
* uiResource: 'ui://my-app/create-form'
|
|
81
|
+
* }, async (params) => {
|
|
82
|
+
* return { id: 'task-123', ...params };
|
|
83
|
+
* });
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export function registerTool<TSchema extends Record<string, z.ZodTypeAny>>(
|
|
87
|
+
name: string,
|
|
88
|
+
options: ToolOptions<TSchema>,
|
|
89
|
+
handler: (
|
|
90
|
+
params: { [K in keyof TSchema]: z.infer<TSchema[K]> },
|
|
91
|
+
extra?: RequestHandlerExtra,
|
|
92
|
+
) => Promise<unknown> | unknown,
|
|
93
|
+
): typeof handler {
|
|
94
|
+
// Register the tool with all services
|
|
95
|
+
for (const service of registry.services) {
|
|
96
|
+
service.registerTool(
|
|
97
|
+
name,
|
|
98
|
+
options.description,
|
|
99
|
+
handler,
|
|
100
|
+
jsonSchemaToParameters(z.toJSONSchema(z.object(options.inputSchema))),
|
|
101
|
+
`/tools/${name.replace(/_/g, "-")}`,
|
|
102
|
+
options.authRequirements
|
|
103
|
+
? [
|
|
104
|
+
new AuthRequirement(
|
|
105
|
+
options.authRequirements.provider,
|
|
106
|
+
options.authRequirements.scopeBundle,
|
|
107
|
+
options.authRequirements.required ?? true,
|
|
108
|
+
),
|
|
109
|
+
]
|
|
110
|
+
: undefined,
|
|
111
|
+
true,
|
|
112
|
+
options.uiResource,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return handler;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Convert JSON Schema to Parameter definitions
|
|
121
|
+
* This converts the output of z.toJSONSchema() back to the Parameter[] format
|
|
122
|
+
* expected by the legacy discovery endpoint
|
|
123
|
+
*/
|
|
124
|
+
function jsonSchemaToParameters(jsonSchema: {
|
|
125
|
+
properties?: Record<string, unknown>;
|
|
126
|
+
required?: string[];
|
|
127
|
+
}): Parameter[] {
|
|
128
|
+
const parameters: Parameter[] = [];
|
|
129
|
+
|
|
130
|
+
if (!jsonSchema.properties) {
|
|
131
|
+
return parameters;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const required = jsonSchema.required || [];
|
|
135
|
+
|
|
136
|
+
for (const [key, value] of Object.entries(jsonSchema.properties)) {
|
|
137
|
+
const prop = value as { description?: string; type?: string };
|
|
138
|
+
parameters.push(
|
|
139
|
+
new Parameter(
|
|
140
|
+
key,
|
|
141
|
+
jsonSchemaTypeToParameterType(prop.type || "string"),
|
|
142
|
+
prop.description || "",
|
|
143
|
+
required.includes(key),
|
|
144
|
+
),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return parameters;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Map JSON Schema type to ParameterType
|
|
153
|
+
*/
|
|
154
|
+
function jsonSchemaTypeToParameterType(jsonType: string): ParameterType {
|
|
155
|
+
switch (jsonType) {
|
|
156
|
+
case "array":
|
|
157
|
+
return ParameterType.List;
|
|
158
|
+
case "boolean":
|
|
159
|
+
return ParameterType.Boolean;
|
|
160
|
+
case "integer":
|
|
161
|
+
return ParameterType.Integer;
|
|
162
|
+
case "number":
|
|
163
|
+
return ParameterType.Number;
|
|
164
|
+
case "object":
|
|
165
|
+
return ParameterType.Dictionary;
|
|
166
|
+
case "string":
|
|
167
|
+
return ParameterType.String;
|
|
168
|
+
default:
|
|
169
|
+
return ParameterType.String;
|
|
170
|
+
}
|
|
171
|
+
}
|
package/src/registry.ts
CHANGED
package/src/service.ts
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
import express, { Express, Request, Response, Router } from
|
|
2
|
-
|
|
3
|
-
import {
|
|
1
|
+
import express, { Express, Request, Response, Router } from "express";
|
|
2
|
+
|
|
3
|
+
import { AuthRequirement, Function, Parameter, Resource } from "./models";
|
|
4
|
+
import { ProteusDocument, UI } from "./proteus";
|
|
5
|
+
import { registry } from "./registry";
|
|
6
|
+
|
|
7
|
+
type ResourceRegistration = {
|
|
8
|
+
handler: () => Promise<ProteusDocument | string> | ProteusDocument | string;
|
|
9
|
+
metadata: Resource;
|
|
10
|
+
};
|
|
4
11
|
|
|
5
12
|
export class ToolsService {
|
|
6
13
|
private app: Express;
|
|
7
|
-
private router: Router;
|
|
8
14
|
private functions: Function[] = [];
|
|
15
|
+
private resources: Map<string, ResourceRegistration> = new Map();
|
|
16
|
+
|
|
17
|
+
private router: Router;
|
|
9
18
|
|
|
10
19
|
/**
|
|
11
20
|
* Initialize a new tools service
|
|
@@ -15,20 +24,34 @@ export class ToolsService {
|
|
|
15
24
|
this.app = app;
|
|
16
25
|
this.router = express.Router();
|
|
17
26
|
this.initRoutes();
|
|
18
|
-
|
|
27
|
+
|
|
19
28
|
// Register this service in the global registry
|
|
20
29
|
registry.services.push(this);
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
/**
|
|
24
|
-
*
|
|
33
|
+
* Register a resource function
|
|
34
|
+
* @param uri The unique URI for this resource (e.g., "ui://my-app/create-form")
|
|
35
|
+
* @param name Name of the resource
|
|
36
|
+
* @param description Description of the resource (optional)
|
|
37
|
+
* @param mimeType MIME type of the resource content (optional)
|
|
38
|
+
* @param title Human-readable title for the resource (optional)
|
|
39
|
+
* @param handler Function implementing the resource (returns string or ProteusDocument)
|
|
25
40
|
*/
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
41
|
+
registerResource(
|
|
42
|
+
uri: string,
|
|
43
|
+
name: string,
|
|
44
|
+
description: string | undefined,
|
|
45
|
+
mimeType: string | undefined,
|
|
46
|
+
title: string | undefined,
|
|
47
|
+
handler: () => Promise<ProteusDocument | string> | ProteusDocument | string,
|
|
48
|
+
): void {
|
|
49
|
+
console.log(`Registering resource: ${name} with URI: ${uri}`);
|
|
30
50
|
|
|
31
|
-
|
|
51
|
+
const metadata = new Resource(uri, name, description, mimeType, title);
|
|
52
|
+
|
|
53
|
+
// Store both metadata and handler together
|
|
54
|
+
this.resources.set(uri, { handler, metadata });
|
|
32
55
|
}
|
|
33
56
|
|
|
34
57
|
/**
|
|
@@ -39,23 +62,35 @@ export class ToolsService {
|
|
|
39
62
|
* @param parameters List of parameters for the tool
|
|
40
63
|
* @param endpoint API endpoint for the tool
|
|
41
64
|
* @param authRequirements Authentication requirements (optional)
|
|
65
|
+
* @param isNewStyle Whether this is a new-style tool (registerTool) vs legacy decorator
|
|
66
|
+
* @param uiResource URI of associated UI resource for dynamic rendering (optional)
|
|
42
67
|
*/
|
|
43
68
|
registerTool(
|
|
44
69
|
name: string,
|
|
45
70
|
description: string,
|
|
71
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
46
72
|
handler: any, // Changed from Function to any to avoid confusion with built-in Function type
|
|
47
73
|
parameters: Parameter[],
|
|
48
74
|
endpoint: string,
|
|
49
|
-
authRequirements?: AuthRequirement[]
|
|
75
|
+
authRequirements?: AuthRequirement[],
|
|
76
|
+
isNewStyle: boolean = false,
|
|
77
|
+
uiResource?: string,
|
|
50
78
|
): void {
|
|
51
|
-
const func = new Function(
|
|
79
|
+
const func = new Function(
|
|
80
|
+
name,
|
|
81
|
+
description,
|
|
82
|
+
parameters,
|
|
83
|
+
endpoint,
|
|
84
|
+
authRequirements,
|
|
85
|
+
uiResource,
|
|
86
|
+
);
|
|
52
87
|
this.functions.push(func);
|
|
53
|
-
|
|
88
|
+
|
|
54
89
|
// Register the actual endpoint
|
|
55
90
|
this.router.post(endpoint, async (req: Request, res: Response) => {
|
|
56
91
|
try {
|
|
57
92
|
console.log(`Received request for ${endpoint}:`, req.body);
|
|
58
|
-
|
|
93
|
+
|
|
59
94
|
// Extract parameters from the request body
|
|
60
95
|
let params;
|
|
61
96
|
if (req.body && req.body.parameters) {
|
|
@@ -64,35 +99,130 @@ export class ToolsService {
|
|
|
64
99
|
console.log(`Extracted parameters from 'parameters' key:`, params);
|
|
65
100
|
} else {
|
|
66
101
|
// Fallback for direct testing: { "name": "value" }
|
|
67
|
-
console.log(
|
|
102
|
+
console.log(
|
|
103
|
+
`Warning: 'parameters' key not found in request body. Using body directly.`,
|
|
104
|
+
);
|
|
68
105
|
params = req.body;
|
|
69
106
|
}
|
|
70
|
-
|
|
107
|
+
|
|
71
108
|
// Extract auth data if available
|
|
72
109
|
const authData = req.body && req.body.auth;
|
|
73
110
|
if (authData) {
|
|
74
|
-
console.log(
|
|
111
|
+
console.log(
|
|
112
|
+
`Auth data provided for provider: ${authData.provider || "unknown"}`,
|
|
113
|
+
);
|
|
75
114
|
}
|
|
76
|
-
|
|
77
|
-
// Call the handler with extracted parameters
|
|
78
|
-
// Check if handler accepts auth as third parameter
|
|
79
|
-
const handlerParamCount = handler.length;
|
|
115
|
+
|
|
116
|
+
// Call the handler with extracted parameters
|
|
80
117
|
let result;
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
118
|
+
|
|
119
|
+
if (isNewStyle) {
|
|
120
|
+
result = await handler(params, {
|
|
121
|
+
mode: (req.body && req.body.execution_mode) || "headless",
|
|
122
|
+
...(authData && { auth: authData }),
|
|
123
|
+
});
|
|
85
124
|
} else {
|
|
86
|
-
//
|
|
87
|
-
|
|
125
|
+
// Check if handler accepts auth as third parameter
|
|
126
|
+
const handlerParamCount = handler.length;
|
|
127
|
+
|
|
128
|
+
if (handlerParamCount >= 2) {
|
|
129
|
+
// Handler accepts auth data
|
|
130
|
+
result = await handler(params, authData);
|
|
131
|
+
} else {
|
|
132
|
+
// Handler doesn't accept auth data
|
|
133
|
+
result = await handler(params);
|
|
134
|
+
}
|
|
88
135
|
}
|
|
89
|
-
|
|
136
|
+
|
|
90
137
|
console.log(`Tool ${name} returned:`, result);
|
|
138
|
+
|
|
91
139
|
res.json(result);
|
|
92
|
-
} catch (error
|
|
140
|
+
} catch (error) {
|
|
93
141
|
console.error(`Error in tool ${name}:`, error);
|
|
94
|
-
res.status(500).json({
|
|
142
|
+
res.status(500).json({
|
|
143
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
144
|
+
});
|
|
95
145
|
}
|
|
96
146
|
});
|
|
97
147
|
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Initialize the discovery endpoint and resources/read endpoint
|
|
151
|
+
*/
|
|
152
|
+
private initRoutes(): void {
|
|
153
|
+
this.router.get("/discovery", (_req: Request, res: Response) => {
|
|
154
|
+
res.json({
|
|
155
|
+
functions: this.functions.map((f) => f.toJSON()),
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// POST /resources/read endpoint for MCP protocol
|
|
160
|
+
this.router.post("/resources/read", async (req: Request, res: Response) => {
|
|
161
|
+
try {
|
|
162
|
+
const { uri } = req.body;
|
|
163
|
+
|
|
164
|
+
if (!uri) {
|
|
165
|
+
return res.status(400).json({
|
|
166
|
+
error: "Missing required field: uri",
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.log(`Received resource read request for URI: ${uri}`);
|
|
171
|
+
|
|
172
|
+
// Find the resource registration
|
|
173
|
+
const registration = this.resources.get(uri);
|
|
174
|
+
|
|
175
|
+
if (!registration) {
|
|
176
|
+
return res.status(404).json({
|
|
177
|
+
error: `Resource not found: ${uri}`,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Call handler and await result
|
|
182
|
+
const content = await registration.handler();
|
|
183
|
+
|
|
184
|
+
let textContent: string;
|
|
185
|
+
let mimeType = registration.metadata.mimeType;
|
|
186
|
+
|
|
187
|
+
// Check if handler returned a ProteusDocument
|
|
188
|
+
if (
|
|
189
|
+
typeof content === "object" &&
|
|
190
|
+
content !== null &&
|
|
191
|
+
"$type" in content &&
|
|
192
|
+
content.$type === "Document"
|
|
193
|
+
) {
|
|
194
|
+
// Auto-serialize to JSON
|
|
195
|
+
textContent = JSON.stringify(content);
|
|
196
|
+
// Auto-set MIME type if not specified
|
|
197
|
+
if (!mimeType) {
|
|
198
|
+
mimeType = UI.MIME_TYPE;
|
|
199
|
+
}
|
|
200
|
+
} else if (typeof content === "string") {
|
|
201
|
+
textContent = content;
|
|
202
|
+
} else {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`Resource handler for '${uri}' must return a string or ProteusDocument, but returned ${typeof content}`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
console.log(
|
|
209
|
+
`Resource ${uri} returned content of length: ${textContent.length}`,
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// Return the resource content directly
|
|
213
|
+
res.json({
|
|
214
|
+
mimeType: mimeType || "text/plain",
|
|
215
|
+
text: textContent,
|
|
216
|
+
uri,
|
|
217
|
+
});
|
|
218
|
+
} catch (error) {
|
|
219
|
+
console.error(`Error reading resource:`, error);
|
|
220
|
+
res.status(500).json({
|
|
221
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
this.app.use(this.router);
|
|
227
|
+
}
|
|
98
228
|
}
|