Skip to content

Commit 72f3699

Browse files
committed
feat: add MCP resources support
- Add listResources, readResource, and listResourceTemplates methods to MCP client - Implement resource handlers in server with proper error handling and retry logic - Add resources directory with remote proxy functionality - Enable resources capability in server initialization - Update README documentation
1 parent 5f17275 commit 72f3699

File tree

5 files changed

+330
-2
lines changed

5 files changed

+330
-2
lines changed

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,6 @@ If your MCP client doesn't support HTTP servers directly, you can use [mcp-remot
113113
> **Note**: Option 1 provides local PDF upload capabilities, while Option 2 only supports PDF processing via URLs (no local file uploads).
114114
115115

116-
117116
## License
118117

119118
This project is licensed under the terms of the MIT open source license. Please refer to [MIT](./LICENSE) for the full terms.

src/client/mcp-client.ts

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ import {
55
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
66
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
77
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
8-
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
8+
import type {
9+
CallToolResult,
10+
ListResourcesResult,
11+
ListResourceTemplatesResult,
12+
ReadResourceResult,
13+
} from '@modelcontextprotocol/sdk/types.js';
914
import pRetry from 'p-retry';
1015
import { CONFIG } from '../config.js';
1116
import { PageIndexOAuthProvider } from './oauth-provider.js';
@@ -192,6 +197,84 @@ export class PageIndexMcpClient {
192197
return tools;
193198
}
194199

200+
/**
201+
* List available resources on the remote server
202+
*/
203+
async listResources(): Promise<ListResourcesResult> {
204+
return pRetry(
205+
async () => {
206+
if (!this.client) {
207+
throw new Error('Client not available');
208+
}
209+
const result = await this.client.listResources();
210+
return result as ListResourcesResult;
211+
},
212+
{
213+
retries: 2,
214+
factor: 1.5,
215+
minTimeout: 500,
216+
maxTimeout: 3000,
217+
onFailedAttempt: (error) => {
218+
console.error(
219+
`List resources attempt ${error.attemptNumber} failed. ${error.retriesLeft} retries left.\n`,
220+
);
221+
},
222+
},
223+
);
224+
}
225+
226+
/**
227+
* Read a specific resource from the remote server
228+
*/
229+
async readResource(uri: string): Promise<ReadResourceResult> {
230+
return pRetry(
231+
async () => {
232+
if (!this.client) {
233+
throw new Error('Client not available');
234+
}
235+
const result = await this.client.readResource({ uri });
236+
return result as ReadResourceResult;
237+
},
238+
{
239+
retries: 2,
240+
factor: 1.5,
241+
minTimeout: 500,
242+
maxTimeout: 3000,
243+
onFailedAttempt: (error) => {
244+
console.error(
245+
`Read resource "${uri}" attempt ${error.attemptNumber} failed. ${error.retriesLeft} retries left.\n`,
246+
);
247+
},
248+
},
249+
);
250+
}
251+
252+
/**
253+
* List resource templates from the remote server
254+
*/
255+
async listResourceTemplates(): Promise<ListResourceTemplatesResult> {
256+
return pRetry(
257+
async () => {
258+
if (!this.client) {
259+
throw new Error('Client not available');
260+
}
261+
const result = await this.client.listResourceTemplates();
262+
return result as ListResourceTemplatesResult;
263+
},
264+
{
265+
retries: 2,
266+
factor: 1.5,
267+
minTimeout: 500,
268+
maxTimeout: 3000,
269+
onFailedAttempt: (error) => {
270+
console.error(
271+
`List resource templates attempt ${error.attemptNumber} failed. ${error.retriesLeft} retries left.\n`,
272+
);
273+
},
274+
},
275+
);
276+
}
277+
195278
/**
196279
* Reconnect to the server
197280
*/

src/resources/index.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import type {
2+
ListResourcesRequest,
3+
ListResourcesResult,
4+
ListResourceTemplatesResult,
5+
ReadResourceRequest,
6+
ReadResourceResult,
7+
} from '@modelcontextprotocol/sdk/types.js';
8+
import type { PageIndexMcpClient } from '../client/mcp-client.js';
9+
import { RemoteResourcesProxy } from './remote-proxy.js';
10+
11+
// Global resources proxy instance
12+
let remoteResourcesProxy: RemoteResourcesProxy | null = null;
13+
14+
/**
15+
* Update the remote resources proxy with a connected MCP client
16+
*/
17+
export function updateResourcesWithRemote(mcpClient: PageIndexMcpClient): void {
18+
remoteResourcesProxy = new RemoteResourcesProxy(mcpClient);
19+
}
20+
21+
/**
22+
* List available resources from the remote PageIndex server
23+
*/
24+
export async function listResources(
25+
_params?: ListResourcesRequest['params'],
26+
): Promise<ListResourcesResult> {
27+
if (!remoteResourcesProxy) {
28+
throw new Error('Remote resources proxy not initialized');
29+
}
30+
31+
try {
32+
const resources = await remoteResourcesProxy.fetchRemoteResources();
33+
return { resources };
34+
} catch (error) {
35+
const errorMessage =
36+
error instanceof Error ? error.message : 'Failed to list resources';
37+
throw new Error(`Failed to list resources: ${errorMessage}`);
38+
}
39+
}
40+
41+
/**
42+
* Read a specific resource from the remote PageIndex server
43+
*/
44+
export async function readResource(
45+
params: ReadResourceRequest['params'],
46+
): Promise<ReadResourceResult> {
47+
if (!remoteResourcesProxy) {
48+
throw new Error('Remote resources proxy not initialized');
49+
}
50+
51+
try {
52+
// Read resource from remote server (let remote server handle URI validation)
53+
return await remoteResourcesProxy.readRemoteResource(params.uri);
54+
} catch (error) {
55+
const errorMessage =
56+
error instanceof Error ? error.message : 'Failed to read resource';
57+
58+
// Return appropriate error codes based on error type
59+
if (
60+
errorMessage.includes('not found') ||
61+
errorMessage.includes('Resource not found')
62+
) {
63+
throw {
64+
code: -32002,
65+
message: errorMessage,
66+
data: { uri: params.uri },
67+
};
68+
}
69+
70+
throw {
71+
code: -32603,
72+
message: errorMessage,
73+
data: { uri: params.uri },
74+
};
75+
}
76+
}
77+
78+
/**
79+
* List available resource templates from remote server
80+
*/
81+
export async function listResourceTemplates(): Promise<ListResourceTemplatesResult> {
82+
if (!remoteResourcesProxy) {
83+
throw new Error('Remote resources proxy not initialized');
84+
}
85+
86+
try {
87+
const resourceTemplates =
88+
await remoteResourcesProxy.fetchRemoteResourceTemplates();
89+
return { resourceTemplates };
90+
} catch (error) {
91+
const errorMessage =
92+
error instanceof Error
93+
? error.message
94+
: 'Failed to list resource templates';
95+
throw new Error(`Failed to list resource templates: ${errorMessage}`);
96+
}
97+
}
98+
99+
export { RemoteResourcesProxy } from './remote-proxy.js';

src/resources/remote-proxy.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type {
2+
ListResourceTemplatesResult,
3+
ReadResourceResult,
4+
Resource,
5+
} from '@modelcontextprotocol/sdk/types.js';
6+
import type { PageIndexMcpClient } from '../client/mcp-client.js';
7+
8+
/**
9+
* Proxy for managing remote PageIndex MCP resources
10+
*/
11+
export class RemoteResourcesProxy {
12+
constructor(private mcpClient: PageIndexMcpClient) {}
13+
14+
/**
15+
* Fetch available resources from remote PageIndex server
16+
*/
17+
async fetchRemoteResources(): Promise<Resource[]> {
18+
try {
19+
// List resources from remote server
20+
const remoteResources = await this.mcpClient.listResources();
21+
return remoteResources.resources;
22+
} catch (error) {
23+
console.error('Failed to fetch remote resources:', error);
24+
return [];
25+
}
26+
}
27+
28+
/**
29+
* Read resource content from remote PageIndex server
30+
*/
31+
async readRemoteResource(uri: string): Promise<ReadResourceResult> {
32+
try {
33+
return await this.mcpClient.readResource(uri);
34+
} catch (error) {
35+
console.error(`Failed to read remote resource ${uri}:`, error);
36+
throw error;
37+
}
38+
}
39+
40+
/**
41+
* Get resource templates from remote server
42+
*/
43+
async fetchRemoteResourceTemplates(): Promise<
44+
ListResourceTemplatesResult['resourceTemplates']
45+
> {
46+
try {
47+
const remoteTemplates = await this.mcpClient.listResourceTemplates();
48+
return remoteTemplates.resourceTemplates;
49+
} catch (error) {
50+
console.error('Failed to fetch remote resource templates:', error);
51+
return [];
52+
}
53+
}
54+
}

src/server.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,19 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
22
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
33
import {
44
CallToolRequestSchema,
5+
ListResourcesRequestSchema,
6+
ListResourceTemplatesRequestSchema,
57
ListToolsRequestSchema,
8+
ReadResourceRequestSchema,
69
} from '@modelcontextprotocol/sdk/types.js';
710
import { zodToJsonSchema } from 'zod-to-json-schema';
811
import { PageIndexMcpClient } from './client/mcp-client.js';
12+
import {
13+
listResources,
14+
listResourceTemplates,
15+
readResource,
16+
updateResourcesWithRemote,
17+
} from './resources/index.js';
918
import {
1019
executeTool,
1120
getTools,
@@ -30,6 +39,7 @@ class PageIndexStdioServer {
3039
{
3140
capabilities: {
3241
tools: {},
42+
resources: {},
3343
},
3444
},
3545
);
@@ -94,6 +104,89 @@ class PageIndexStdioServer {
94104
}
95105
});
96106

107+
// Resource handlers
108+
this.server.setRequestHandler(
109+
ListResourcesRequestSchema,
110+
async (request) => {
111+
// Initialize remote connection on first list resources request
112+
if (!this.mcpClient) {
113+
await this.connectToRemoteServer();
114+
}
115+
116+
// biome-ignore lint/style/noNonNullAssertion: mcpClient is ensured to be non-null here
117+
updateResourcesWithRemote(this.mcpClient!);
118+
119+
try {
120+
return await listResources(request.params);
121+
} catch (error) {
122+
throw {
123+
code: -32603,
124+
message:
125+
error instanceof Error
126+
? error.message
127+
: 'Failed to list resources',
128+
data: { cursor: request.params?.cursor },
129+
};
130+
}
131+
},
132+
);
133+
134+
this.server.setRequestHandler(
135+
ReadResourceRequestSchema,
136+
async (request) => {
137+
// Ensure connection is established
138+
if (!this.mcpClient) {
139+
await this.connectToRemoteServer();
140+
}
141+
142+
// biome-ignore lint/style/noNonNullAssertion: mcpClient is ensured to be non-null here
143+
updateResourcesWithRemote(this.mcpClient!);
144+
145+
try {
146+
return await readResource(request.params);
147+
} catch (error) {
148+
// Re-throw MCP-formatted errors directly
149+
if (typeof error === 'object' && error !== null && 'code' in error) {
150+
throw error;
151+
}
152+
153+
throw {
154+
code: -32603,
155+
message:
156+
error instanceof Error
157+
? error.message
158+
: 'Failed to read resource',
159+
data: { uri: request.params.uri },
160+
};
161+
}
162+
},
163+
);
164+
165+
this.server.setRequestHandler(
166+
ListResourceTemplatesRequestSchema,
167+
async () => {
168+
// Ensure connection is established
169+
if (!this.mcpClient) {
170+
await this.connectToRemoteServer();
171+
}
172+
173+
// biome-ignore lint/style/noNonNullAssertion: mcpClient is ensured to be non-null here
174+
updateResourcesWithRemote(this.mcpClient!);
175+
176+
try {
177+
return await listResourceTemplates();
178+
} catch (error) {
179+
throw {
180+
code: -32603,
181+
message:
182+
error instanceof Error
183+
? error.message
184+
: 'Failed to list resource templates',
185+
};
186+
}
187+
},
188+
);
189+
97190
this.server.onerror = (error) => {
98191
console.error(`MCP Server error: ${error}\n`);
99192
};

0 commit comments

Comments
 (0)