@mastra/mcp 0.10.2 → 0.10.3
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/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +31 -0
- package/README.md +32 -0
- package/dist/_tsup-dts-rollup.d.cts +175 -1
- package/dist/_tsup-dts-rollup.d.ts +175 -1
- package/dist/index.cjs +251 -2
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +253 -4
- package/integration-tests/node_modules/.bin/vitest +2 -2
- package/integration-tests/package.json +3 -3
- package/package.json +9 -9
- package/src/__fixtures__/weather.ts +62 -2
- package/src/client/client.test.ts +46 -0
- package/src/client/client.ts +43 -1
- package/src/client/configuration.test.ts +280 -168
- package/src/client/configuration.ts +27 -1
- package/src/client/index.ts +3 -0
- package/src/client/promptActions.ts +70 -0
- package/src/client/resourceActions.ts +0 -2
- package/src/index.ts +2 -4
- package/src/server/index.ts +2 -0
- package/src/server/promptActions.ts +37 -0
- package/src/server/server.test.ts +224 -1
- package/src/server/server.ts +139 -14
- package/src/server/types.ts +30 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { IMastraLogger } from "@mastra/core/logger";
|
|
2
|
+
import { ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import type { GetPromptResult, Prompt } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import type { InternalMastraMCPClient } from "./client";
|
|
5
|
+
|
|
6
|
+
interface PromptClientActionsConfig {
|
|
7
|
+
client: InternalMastraMCPClient;
|
|
8
|
+
logger: IMastraLogger;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Client-side prompt actions for listing, getting, and subscribing to prompt changes.
|
|
13
|
+
*/
|
|
14
|
+
export class PromptClientActions {
|
|
15
|
+
private readonly client: InternalMastraMCPClient;
|
|
16
|
+
private readonly logger: IMastraLogger;
|
|
17
|
+
|
|
18
|
+
constructor({ client, logger }: PromptClientActionsConfig) {
|
|
19
|
+
this.client = client;
|
|
20
|
+
this.logger = logger;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get all prompts from the connected MCP server.
|
|
25
|
+
* @returns A list of prompts with their versions.
|
|
26
|
+
*/
|
|
27
|
+
public async list(): Promise<Prompt[]> {
|
|
28
|
+
try {
|
|
29
|
+
const response = await this.client.listPrompts();
|
|
30
|
+
if (response && response.prompts && Array.isArray(response.prompts)) {
|
|
31
|
+
return response.prompts.map((prompt) => ({ ...prompt, version: prompt.version || '' }));
|
|
32
|
+
} else {
|
|
33
|
+
this.logger.warn(`Prompts response from server ${this.client.name} did not have expected structure.`, {
|
|
34
|
+
response,
|
|
35
|
+
});
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
} catch (e: any) {
|
|
39
|
+
// MCP Server might not support prompts, so we return an empty array
|
|
40
|
+
if (e.code === ErrorCode.MethodNotFound) {
|
|
41
|
+
return []
|
|
42
|
+
}
|
|
43
|
+
this.logger.error(`Error getting prompts from server ${this.client.name}`, {
|
|
44
|
+
error: e instanceof Error ? e.message : String(e),
|
|
45
|
+
});
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Failed to fetch prompts from server ${this.client.name}: ${e instanceof Error ? e.stack || e.message : String(e)}`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get a specific prompt.
|
|
54
|
+
* @param name The name of the prompt to get.
|
|
55
|
+
* @param args Optional arguments for the prompt.
|
|
56
|
+
* @param version Optional version of the prompt to get.
|
|
57
|
+
* @returns The prompt content.
|
|
58
|
+
*/
|
|
59
|
+
public async get({name, args, version}: {name: string, args?: Record<string, any>, version?: string}): Promise<GetPromptResult> {
|
|
60
|
+
return this.client.getPrompt({name, args, version});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Set a notification handler for when the list of available prompts changes.
|
|
65
|
+
* @param handler The callback function to handle the notification.
|
|
66
|
+
*/
|
|
67
|
+
public async onListChanged(handler: () => void): Promise<void> {
|
|
68
|
+
this.client.setPromptListChangedNotificationHandler(handler);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -40,7 +40,6 @@ export class ResourceClientActions {
|
|
|
40
40
|
this.logger.error(`Error getting resources from server ${this.client.name}`, {
|
|
41
41
|
error: e instanceof Error ? e.message : String(e),
|
|
42
42
|
});
|
|
43
|
-
console.log('errorheere', e)
|
|
44
43
|
throw new Error(
|
|
45
44
|
`Failed to fetch resources from server ${this.client.name}: ${e instanceof Error ? e.stack || e.message : String(e)}`,
|
|
46
45
|
);
|
|
@@ -65,7 +64,6 @@ export class ResourceClientActions {
|
|
|
65
64
|
}
|
|
66
65
|
} catch (e: any) {
|
|
67
66
|
// MCP Server might not support resources, so we return an empty array
|
|
68
|
-
console.log({ errorcooode: e.code })
|
|
69
67
|
if (e.code === ErrorCode.MethodNotFound) {
|
|
70
68
|
return []
|
|
71
69
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,2 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export * from './client/configuration';
|
|
4
|
-
export * from './server/server';
|
|
1
|
+
export * from './client';
|
|
2
|
+
export * from './server';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { IMastraLogger } from '@mastra/core/logger';
|
|
2
|
+
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
|
|
4
|
+
interface ServerPromptActionsDependencies {
|
|
5
|
+
getLogger: () => IMastraLogger;
|
|
6
|
+
getSdkServer: () => Server;
|
|
7
|
+
clearDefinedPrompts: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class ServerPromptActions {
|
|
11
|
+
private readonly getLogger: () => IMastraLogger;
|
|
12
|
+
private readonly getSdkServer: () => Server;
|
|
13
|
+
private readonly clearDefinedPrompts: () => void;
|
|
14
|
+
|
|
15
|
+
constructor(dependencies: ServerPromptActionsDependencies) {
|
|
16
|
+
this.getLogger = dependencies.getLogger;
|
|
17
|
+
this.getSdkServer = dependencies.getSdkServer;
|
|
18
|
+
this.clearDefinedPrompts = dependencies.clearDefinedPrompts;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Notifies the server that the overall list of available prompts has changed.
|
|
23
|
+
* This will clear the internal cache of defined prompts and send a list_changed notification to clients.
|
|
24
|
+
*/
|
|
25
|
+
public async notifyListChanged(): Promise<void> {
|
|
26
|
+
this.getLogger().info('Prompt list change externally notified. Clearing definedPrompts and sending notification.');
|
|
27
|
+
this.clearDefinedPrompts();
|
|
28
|
+
try {
|
|
29
|
+
await this.getSdkServer().sendPromptListChanged();
|
|
30
|
+
} catch (error) {
|
|
31
|
+
this.getLogger().error('Failed to send prompt list changed notification:', {
|
|
32
|
+
error: error instanceof Error ? error.message : String(error),
|
|
33
|
+
});
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -13,6 +13,8 @@ import type {
|
|
|
13
13
|
ListResourcesResult,
|
|
14
14
|
ReadResourceResult,
|
|
15
15
|
ListResourceTemplatesResult,
|
|
16
|
+
GetPromptResult,
|
|
17
|
+
Prompt,
|
|
16
18
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
17
19
|
import { MockLanguageModelV1 } from 'ai/test';
|
|
18
20
|
import { Hono } from 'hono';
|
|
@@ -22,7 +24,7 @@ import { weatherTool } from '../__fixtures__/tools';
|
|
|
22
24
|
import { InternalMastraMCPClient } from '../client/client';
|
|
23
25
|
import { MCPClient } from '../client/configuration';
|
|
24
26
|
import { MCPServer } from './server';
|
|
25
|
-
import type { MCPServerResources, MCPServerResourceContent } from './
|
|
27
|
+
import type { MCPServerResources, MCPServerResourceContent } from './types';
|
|
26
28
|
|
|
27
29
|
const PORT = 9100 + Math.floor(Math.random() * 1000);
|
|
28
30
|
let server: MCPServer;
|
|
@@ -607,6 +609,227 @@ describe('MCPServer', () => {
|
|
|
607
609
|
});
|
|
608
610
|
});
|
|
609
611
|
|
|
612
|
+
describe('Prompts', () => {
|
|
613
|
+
let promptServer: MCPServer;
|
|
614
|
+
let promptInternalClient: InternalMastraMCPClient;
|
|
615
|
+
let promptHttpServer: http.Server;
|
|
616
|
+
const PROMPT_PORT = 9500 + Math.floor(Math.random() * 1000);
|
|
617
|
+
|
|
618
|
+
let currentPrompts: Prompt[] = [
|
|
619
|
+
{
|
|
620
|
+
name: 'explain-code',
|
|
621
|
+
version: 'v1',
|
|
622
|
+
description: 'Explain code v1',
|
|
623
|
+
arguments: [{ name: 'code', required: true }],
|
|
624
|
+
getMessages: async (args: any) => [
|
|
625
|
+
{ role: 'user', content: { type: 'text', text: `Explain this code (v1):\n${args.code}` } },
|
|
626
|
+
],
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
name: 'explain-code',
|
|
630
|
+
version: 'v2',
|
|
631
|
+
description: 'Explain code v2',
|
|
632
|
+
arguments: [{ name: 'code', required: true }],
|
|
633
|
+
getMessages: async (args: any) => [
|
|
634
|
+
{ role: 'user', content: { type: 'text', text: `Explain this code (v2):\n${args.code}` } },
|
|
635
|
+
],
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
name: 'summarize',
|
|
639
|
+
version: 'v1',
|
|
640
|
+
description: 'Summarize text',
|
|
641
|
+
arguments: [{ name: 'text', required: true }],
|
|
642
|
+
getMessages: async (args: any) => [
|
|
643
|
+
{ role: 'user', content: { type: 'text', text: `Summarize this:\n${args.text}` } },
|
|
644
|
+
],
|
|
645
|
+
},
|
|
646
|
+
];
|
|
647
|
+
|
|
648
|
+
beforeAll(async () => {
|
|
649
|
+
// Register multiple versions of the same prompt
|
|
650
|
+
|
|
651
|
+
promptServer = new MCPServer({
|
|
652
|
+
name: 'PromptTestServer',
|
|
653
|
+
version: '1.0.0',
|
|
654
|
+
tools: {},
|
|
655
|
+
prompts: {
|
|
656
|
+
listPrompts: async () => currentPrompts,
|
|
657
|
+
getPromptMessages: async (params: { name: string; version?: string; args?: any }) => {
|
|
658
|
+
let prompt;
|
|
659
|
+
if (params.version) {
|
|
660
|
+
prompt = currentPrompts.find(p => p.name === params.name && p.version === params.version);
|
|
661
|
+
} else {
|
|
662
|
+
// Select the first matching name if no version is provided.
|
|
663
|
+
prompt = currentPrompts.find(p => p.name === params.name);
|
|
664
|
+
}
|
|
665
|
+
if (!prompt)
|
|
666
|
+
throw new Error(
|
|
667
|
+
`Prompt "${params.name}"${params.version ? ` (version ${params.version})` : ''} not found`,
|
|
668
|
+
);
|
|
669
|
+
return (prompt as any).getMessages(params.args);
|
|
670
|
+
},
|
|
671
|
+
},
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
promptHttpServer = http.createServer(async (req: http.IncomingMessage, res: http.ServerResponse) => {
|
|
675
|
+
const url = new URL(req.url || '', `http://localhost:${PROMPT_PORT}`);
|
|
676
|
+
await promptServer.startSSE({
|
|
677
|
+
url,
|
|
678
|
+
ssePath: '/sse',
|
|
679
|
+
messagePath: '/messages',
|
|
680
|
+
req,
|
|
681
|
+
res,
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
await new Promise<void>(resolve => promptHttpServer.listen(PROMPT_PORT, () => resolve()));
|
|
685
|
+
promptInternalClient = new InternalMastraMCPClient({
|
|
686
|
+
name: 'prompt-test-internal-client',
|
|
687
|
+
server: { url: new URL(`http://localhost:${PROMPT_PORT}/sse`) },
|
|
688
|
+
});
|
|
689
|
+
await promptInternalClient.connect();
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
afterAll(async () => {
|
|
693
|
+
await promptInternalClient.disconnect();
|
|
694
|
+
if (promptHttpServer) {
|
|
695
|
+
promptHttpServer.closeAllConnections?.();
|
|
696
|
+
await new Promise<void>((resolve, reject) => {
|
|
697
|
+
promptHttpServer.close(err => {
|
|
698
|
+
if (err) return reject(err);
|
|
699
|
+
resolve();
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
if (promptServer) {
|
|
704
|
+
await promptServer.close();
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
it('should send prompt list changed notification when prompts change', async () => {
|
|
709
|
+
const listChangedPromise = new Promise<void>(resolve => {
|
|
710
|
+
promptInternalClient.setPromptListChangedNotificationHandler(() => {
|
|
711
|
+
resolve();
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
await promptServer.prompts.notifyListChanged();
|
|
715
|
+
|
|
716
|
+
await expect(listChangedPromise).resolves.toBeUndefined(); // Wait for the notification
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it('should list all prompts with version field', async () => {
|
|
720
|
+
const result = await promptInternalClient.listPrompts();
|
|
721
|
+
expect(result).toBeDefined();
|
|
722
|
+
expect(result.prompts).toBeInstanceOf(Array);
|
|
723
|
+
// Should contain both explain-code v1 and v2 and summarize v1
|
|
724
|
+
const explainV1 = result.prompts.find((p: Prompt) => p.name === 'explain-code' && p.version === 'v1');
|
|
725
|
+
const explainV2 = result.prompts.find((p: Prompt) => p.name === 'explain-code' && p.version === 'v2');
|
|
726
|
+
const summarizeV1 = result.prompts.find((p: Prompt) => p.name === 'summarize' && p.version === 'v1');
|
|
727
|
+
expect(explainV1).toBeDefined();
|
|
728
|
+
expect(explainV2).toBeDefined();
|
|
729
|
+
expect(summarizeV1).toBeDefined();
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
it('should retrieve prompt by name and version', async () => {
|
|
733
|
+
const result = await promptInternalClient.getPrompt({
|
|
734
|
+
name: 'explain-code',
|
|
735
|
+
args: { code: 'let x = 1;' },
|
|
736
|
+
version: 'v2',
|
|
737
|
+
});
|
|
738
|
+
const prompt = result.prompt as GetPromptResult;
|
|
739
|
+
expect(prompt).toBeDefined();
|
|
740
|
+
expect(prompt.name).toBe('explain-code');
|
|
741
|
+
expect(prompt.version).toBe('v2');
|
|
742
|
+
|
|
743
|
+
const messages = result.messages;
|
|
744
|
+
expect(messages).toBeDefined();
|
|
745
|
+
expect(messages.length).toBeGreaterThan(0);
|
|
746
|
+
expect(messages[0].content.text).toContain('(v2)');
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
it('should retrieve prompt by name and default to first version if not specified', async () => {
|
|
750
|
+
const result = await promptInternalClient.getPrompt({ name: 'explain-code', args: { code: 'let y = 2;' } });
|
|
751
|
+
expect(result.prompt).toBeDefined();
|
|
752
|
+
const prompt = result.prompt as GetPromptResult;
|
|
753
|
+
expect(prompt.name).toBe('explain-code');
|
|
754
|
+
// Should default to first version (v1)
|
|
755
|
+
expect(prompt.version).toBe('v1');
|
|
756
|
+
|
|
757
|
+
const messages = result.messages;
|
|
758
|
+
expect(messages).toBeDefined();
|
|
759
|
+
expect(messages.length).toBeGreaterThan(0);
|
|
760
|
+
expect(messages[0].content.text).toContain('(v1)');
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
it('should return error if prompt name/version does not exist', async () => {
|
|
764
|
+
await expect(
|
|
765
|
+
promptInternalClient.getPrompt({ name: 'explain-code', args: { code: 'foo' }, version: 'v999' }),
|
|
766
|
+
).rejects.toThrow();
|
|
767
|
+
});
|
|
768
|
+
it('should throw error if required argument is missing', async () => {
|
|
769
|
+
await expect(
|
|
770
|
+
promptInternalClient.getPrompt({ name: 'explain-code', args: {} }), // missing 'code'
|
|
771
|
+
).rejects.toThrow(/Missing required argument/);
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
it('should succeed if all required arguments are provided', async () => {
|
|
775
|
+
const result = await promptInternalClient.getPrompt({ name: 'explain-code', args: { code: 'let z = 3;' } });
|
|
776
|
+
expect(result.prompt).toBeDefined();
|
|
777
|
+
expect(result.messages[0].content.text).toContain('let z = 3;');
|
|
778
|
+
});
|
|
779
|
+
it('should allow prompts with optional arguments', async () => {
|
|
780
|
+
// Register a prompt with an optional argument
|
|
781
|
+
currentPrompts = [
|
|
782
|
+
{
|
|
783
|
+
name: 'optional-arg-prompt',
|
|
784
|
+
version: 'v1',
|
|
785
|
+
description: 'Prompt with optional argument',
|
|
786
|
+
arguments: [{ name: 'foo', required: false }],
|
|
787
|
+
getMessages: async (args: any) => [
|
|
788
|
+
{ role: 'user', content: { type: 'text', text: `foo is: ${args.foo ?? 'none'}` } },
|
|
789
|
+
],
|
|
790
|
+
},
|
|
791
|
+
];
|
|
792
|
+
await promptServer.prompts.notifyListChanged();
|
|
793
|
+
const result = await promptInternalClient.getPrompt({ name: 'optional-arg-prompt', args: {} });
|
|
794
|
+
expect(result.prompt).toBeDefined();
|
|
795
|
+
expect(result.messages[0].content.text).toContain('foo is: none');
|
|
796
|
+
});
|
|
797
|
+
it('should retrieve prompt with no version field by name only', async () => {
|
|
798
|
+
currentPrompts = [
|
|
799
|
+
{
|
|
800
|
+
name: 'no-version',
|
|
801
|
+
description: 'Prompt without version',
|
|
802
|
+
arguments: [],
|
|
803
|
+
getMessages: async () => [{ role: 'user', content: { type: 'text', text: 'no version' } }],
|
|
804
|
+
},
|
|
805
|
+
];
|
|
806
|
+
await promptServer.prompts.notifyListChanged();
|
|
807
|
+
const result = await promptInternalClient.getPrompt({ name: 'no-version', args: {} });
|
|
808
|
+
const prompt = result.prompt as GetPromptResult;
|
|
809
|
+
expect(prompt).toBeDefined();
|
|
810
|
+
expect(prompt.version).toBeUndefined();
|
|
811
|
+
const messages = result.messages;
|
|
812
|
+
expect(messages).toBeDefined();
|
|
813
|
+
expect(messages.length).toBeGreaterThan(0);
|
|
814
|
+
expect(messages[0].content.text).toContain('no version');
|
|
815
|
+
});
|
|
816
|
+
it('should list prompts with required fields', async () => {
|
|
817
|
+
const result = await promptInternalClient.listPrompts();
|
|
818
|
+
result.prompts.forEach((p: Prompt) => {
|
|
819
|
+
expect(p.name).toBeDefined();
|
|
820
|
+
expect(p.description).toBeDefined();
|
|
821
|
+
expect(p.arguments).toBeDefined();
|
|
822
|
+
});
|
|
823
|
+
});
|
|
824
|
+
it('should return empty list if no prompts are registered', async () => {
|
|
825
|
+
currentPrompts = [];
|
|
826
|
+
await promptServer.prompts.notifyListChanged();
|
|
827
|
+
const result = await promptInternalClient.listPrompts();
|
|
828
|
+
expect(result.prompts).toBeInstanceOf(Array);
|
|
829
|
+
expect(result.prompts.length).toBe(0);
|
|
830
|
+
});
|
|
831
|
+
});
|
|
832
|
+
|
|
610
833
|
describe('MCPServer SSE transport', () => {
|
|
611
834
|
let sseRes: Response | undefined;
|
|
612
835
|
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
|
package/src/server/server.ts
CHANGED
|
@@ -29,32 +29,30 @@ import {
|
|
|
29
29
|
ListResourceTemplatesRequestSchema,
|
|
30
30
|
SubscribeRequestSchema,
|
|
31
31
|
UnsubscribeRequestSchema,
|
|
32
|
+
ListPromptsRequestSchema,
|
|
33
|
+
GetPromptRequestSchema,
|
|
34
|
+
PromptSchema,
|
|
32
35
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
33
36
|
import type {
|
|
34
37
|
ResourceContents,
|
|
35
38
|
Resource,
|
|
36
39
|
ResourceTemplate,
|
|
37
40
|
ServerCapabilities,
|
|
41
|
+
Prompt,
|
|
38
42
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
39
43
|
import type { SSEStreamingApi } from 'hono/streaming';
|
|
40
44
|
import { streamSSE } from 'hono/streaming';
|
|
41
45
|
import { SSETransport } from 'hono-mcp-server-sse-transport';
|
|
42
46
|
import { z } from 'zod';
|
|
47
|
+
import { ServerPromptActions } from './promptActions';
|
|
43
48
|
import { ServerResourceActions } from './resourceActions';
|
|
49
|
+
import type {
|
|
50
|
+
MCPServerPromptMessagesCallback,
|
|
51
|
+
MCPServerPrompts,
|
|
52
|
+
MCPServerResourceContentCallback,
|
|
53
|
+
MCPServerResources,
|
|
54
|
+
} from './types';
|
|
44
55
|
|
|
45
|
-
export type MCPServerResourceContentCallback = ({
|
|
46
|
-
uri,
|
|
47
|
-
}: {
|
|
48
|
-
uri: string;
|
|
49
|
-
}) => Promise<MCPServerResourceContent | MCPServerResourceContent[]>;
|
|
50
|
-
export type MCPServerResourceContent = { text?: string } | { blob?: string };
|
|
51
|
-
export type MCPServerResources = {
|
|
52
|
-
listResources: () => Promise<Resource[]>;
|
|
53
|
-
getResourceContent: MCPServerResourceContentCallback;
|
|
54
|
-
resourceTemplates?: () => Promise<ResourceTemplate[]>;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export type { Resource, ResourceTemplate };
|
|
58
56
|
export class MCPServer extends MCPServerBase {
|
|
59
57
|
private server: Server;
|
|
60
58
|
private stdioTransport?: StdioServerTransport;
|
|
@@ -69,11 +67,17 @@ export class MCPServer extends MCPServerBase {
|
|
|
69
67
|
private subscribeResourceHandlerIsRegistered: boolean = false;
|
|
70
68
|
private unsubscribeResourceHandlerIsRegistered: boolean = false;
|
|
71
69
|
|
|
70
|
+
private listPromptsHandlerIsRegistered: boolean = false;
|
|
71
|
+
private getPromptHandlerIsRegistered: boolean = false;
|
|
72
|
+
|
|
72
73
|
private definedResources?: Resource[];
|
|
73
74
|
private definedResourceTemplates?: ResourceTemplate[];
|
|
74
75
|
private resourceOptions?: MCPServerResources;
|
|
76
|
+
private definedPrompts?: Prompt[];
|
|
77
|
+
private promptOptions?: MCPServerPrompts;
|
|
75
78
|
private subscriptions: Set<string> = new Set();
|
|
76
79
|
public readonly resources: ServerResourceActions;
|
|
80
|
+
public readonly prompts: ServerPromptActions;
|
|
77
81
|
|
|
78
82
|
/**
|
|
79
83
|
* Get the current stdio transport.
|
|
@@ -114,9 +118,10 @@ export class MCPServer extends MCPServerBase {
|
|
|
114
118
|
* Construct a new MCPServer instance.
|
|
115
119
|
* @param opts - Configuration options for the server, including registry metadata.
|
|
116
120
|
*/
|
|
117
|
-
constructor(opts: MCPServerConfig & { resources?: MCPServerResources }) {
|
|
121
|
+
constructor(opts: MCPServerConfig & { resources?: MCPServerResources; prompts?: MCPServerPrompts }) {
|
|
118
122
|
super(opts);
|
|
119
123
|
this.resourceOptions = opts.resources;
|
|
124
|
+
this.promptOptions = opts.prompts;
|
|
120
125
|
|
|
121
126
|
const capabilities: ServerCapabilities = {
|
|
122
127
|
tools: {},
|
|
@@ -127,6 +132,10 @@ export class MCPServer extends MCPServerBase {
|
|
|
127
132
|
capabilities.resources = { subscribe: true, listChanged: true };
|
|
128
133
|
}
|
|
129
134
|
|
|
135
|
+
if (opts.prompts) {
|
|
136
|
+
capabilities.prompts = { listChanged: true };
|
|
137
|
+
}
|
|
138
|
+
|
|
130
139
|
this.server = new Server({ name: this.name, version: this.version }, { capabilities });
|
|
131
140
|
|
|
132
141
|
this.logger.info(
|
|
@@ -146,6 +155,13 @@ export class MCPServer extends MCPServerBase {
|
|
|
146
155
|
this.registerListResourceTemplatesHandler();
|
|
147
156
|
}
|
|
148
157
|
}
|
|
158
|
+
if (opts.prompts) {
|
|
159
|
+
this.registerListPromptsHandler();
|
|
160
|
+
this.registerGetPromptHandler({
|
|
161
|
+
getPromptMessagesCallback: opts.prompts.getPromptMessages,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
149
165
|
this.resources = new ServerResourceActions({
|
|
150
166
|
getSubscriptions: () => this.subscriptions,
|
|
151
167
|
getLogger: () => this.logger,
|
|
@@ -157,6 +173,14 @@ export class MCPServer extends MCPServerBase {
|
|
|
157
173
|
this.definedResourceTemplates = undefined;
|
|
158
174
|
},
|
|
159
175
|
});
|
|
176
|
+
|
|
177
|
+
this.prompts = new ServerPromptActions({
|
|
178
|
+
getLogger: () => this.logger,
|
|
179
|
+
getSdkServer: () => this.server,
|
|
180
|
+
clearDefinedPrompts: () => {
|
|
181
|
+
this.definedPrompts = undefined;
|
|
182
|
+
},
|
|
183
|
+
});
|
|
160
184
|
}
|
|
161
185
|
|
|
162
186
|
private convertAgentsToTools(
|
|
@@ -654,6 +678,107 @@ export class MCPServer extends MCPServerBase {
|
|
|
654
678
|
});
|
|
655
679
|
}
|
|
656
680
|
|
|
681
|
+
/**
|
|
682
|
+
* Register the ListPrompts handler.
|
|
683
|
+
*/
|
|
684
|
+
private registerListPromptsHandler() {
|
|
685
|
+
if (this.listPromptsHandlerIsRegistered) {
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
this.listPromptsHandlerIsRegistered = true;
|
|
689
|
+
const capturedPromptOptions = this.promptOptions;
|
|
690
|
+
|
|
691
|
+
if (!capturedPromptOptions?.listPrompts) {
|
|
692
|
+
this.logger.warn('ListPrompts capability not supported by server configuration.');
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
this.server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
697
|
+
this.logger.debug('Handling ListPrompts request');
|
|
698
|
+
if (this.definedPrompts) {
|
|
699
|
+
return {
|
|
700
|
+
prompts: this.definedPrompts?.map(p => ({ ...p, version: p.version ?? undefined })),
|
|
701
|
+
};
|
|
702
|
+
} else {
|
|
703
|
+
try {
|
|
704
|
+
const prompts = await capturedPromptOptions.listPrompts();
|
|
705
|
+
// Parse and cache the prompts
|
|
706
|
+
for (const prompt of prompts) {
|
|
707
|
+
PromptSchema.parse(prompt);
|
|
708
|
+
}
|
|
709
|
+
this.definedPrompts = prompts;
|
|
710
|
+
this.logger.debug(`Fetched and cached ${this.definedPrompts.length} prompts.`);
|
|
711
|
+
return {
|
|
712
|
+
prompts: this.definedPrompts?.map(p => ({ ...p, version: p.version ?? undefined })),
|
|
713
|
+
};
|
|
714
|
+
} catch (error) {
|
|
715
|
+
this.logger.error('Error fetching prompts via listPrompts():', {
|
|
716
|
+
error: error instanceof Error ? error.message : String(error),
|
|
717
|
+
});
|
|
718
|
+
// Re-throw to let the MCP Server SDK handle formatting the error response
|
|
719
|
+
throw error;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Register the GetPrompt handler.
|
|
727
|
+
*/
|
|
728
|
+
private registerGetPromptHandler({
|
|
729
|
+
getPromptMessagesCallback,
|
|
730
|
+
}: {
|
|
731
|
+
getPromptMessagesCallback?: MCPServerPromptMessagesCallback;
|
|
732
|
+
}) {
|
|
733
|
+
if (this.getPromptHandlerIsRegistered) return;
|
|
734
|
+
this.getPromptHandlerIsRegistered = true;
|
|
735
|
+
// Accept optional version parameter in prompts/get
|
|
736
|
+
this.server.setRequestHandler(
|
|
737
|
+
GetPromptRequestSchema,
|
|
738
|
+
async (request: { params: { name: string; version?: string; arguments?: any } }) => {
|
|
739
|
+
const startTime = Date.now();
|
|
740
|
+
const { name, version, arguments: args } = request.params;
|
|
741
|
+
if (!this.definedPrompts) {
|
|
742
|
+
const prompts = await this.promptOptions?.listPrompts?.();
|
|
743
|
+
if (!prompts) throw new Error('Failed to load prompts');
|
|
744
|
+
this.definedPrompts = prompts;
|
|
745
|
+
}
|
|
746
|
+
// Select prompt by name and version (if provided)
|
|
747
|
+
let prompt;
|
|
748
|
+
if (version) {
|
|
749
|
+
prompt = this.definedPrompts?.find(p => p.name === name && p.version === version);
|
|
750
|
+
} else {
|
|
751
|
+
// Select the first matching name if no version is provided.
|
|
752
|
+
prompt = this.definedPrompts?.find(p => p.name === name);
|
|
753
|
+
}
|
|
754
|
+
if (!prompt) throw new Error(`Prompt "${name}"${version ? ` (version ${version})` : ''} not found`);
|
|
755
|
+
// Validate required arguments
|
|
756
|
+
if (prompt.arguments) {
|
|
757
|
+
for (const arg of prompt.arguments) {
|
|
758
|
+
if (arg.required && (args?.[arg.name] === undefined || args?.[arg.name] === null)) {
|
|
759
|
+
throw new Error(`Missing required argument: ${arg.name}`);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
try {
|
|
764
|
+
let messages: any[] = [];
|
|
765
|
+
if (getPromptMessagesCallback) {
|
|
766
|
+
messages = await getPromptMessagesCallback({ name, version, args });
|
|
767
|
+
}
|
|
768
|
+
const duration = Date.now() - startTime;
|
|
769
|
+
this.logger.info(
|
|
770
|
+
`Prompt '${name}'${version ? ` (version ${version})` : ''} retrieved successfully in ${duration}ms.`,
|
|
771
|
+
);
|
|
772
|
+
return { prompt, messages };
|
|
773
|
+
} catch (error) {
|
|
774
|
+
const duration = Date.now() - startTime;
|
|
775
|
+
this.logger.error(`Failed to get content for prompt '${name}' in ${duration}ms`, { error });
|
|
776
|
+
throw error;
|
|
777
|
+
}
|
|
778
|
+
},
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
|
|
657
782
|
/**
|
|
658
783
|
* Start the MCP server using stdio transport (for Windsurf integration).
|
|
659
784
|
*/
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Prompt, PromptMessage, Resource, ResourceTemplate } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
|
|
3
|
+
export type MCPServerResourceContentCallback = ({
|
|
4
|
+
uri,
|
|
5
|
+
}: {
|
|
6
|
+
uri: string;
|
|
7
|
+
}) => Promise<MCPServerResourceContent | MCPServerResourceContent[]>;
|
|
8
|
+
export type MCPServerResourceContent = { text?: string } | { blob?: string };
|
|
9
|
+
export type MCPServerResources = {
|
|
10
|
+
listResources: () => Promise<Resource[]>;
|
|
11
|
+
getResourceContent: MCPServerResourceContentCallback;
|
|
12
|
+
resourceTemplates?: () => Promise<ResourceTemplate[]>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type MCPServerPromptMessagesCallback = ({
|
|
16
|
+
name,
|
|
17
|
+
version,
|
|
18
|
+
args,
|
|
19
|
+
}: {
|
|
20
|
+
name: string;
|
|
21
|
+
version?: string;
|
|
22
|
+
args?: any;
|
|
23
|
+
}) => Promise<PromptMessage[]>;
|
|
24
|
+
|
|
25
|
+
export type MCPServerPrompts = {
|
|
26
|
+
listPrompts: () => Promise<Prompt[]>;
|
|
27
|
+
getPromptMessages?: MCPServerPromptMessagesCallback;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type { Resource, ResourceTemplate };
|