@portel/photon-core 1.2.0 → 1.3.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 +81 -0
- package/dist/base.d.ts +67 -0
- package/dist/base.d.ts.map +1 -1
- package/dist/base.js +92 -0
- package/dist/base.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp-client.d.ts +179 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +211 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/mcp-sdk-transport.d.ts +88 -0
- package/dist/mcp-sdk-transport.d.ts.map +1 -0
- package/dist/mcp-sdk-transport.js +360 -0
- package/dist/mcp-sdk-transport.js.map +1 -0
- package/dist/schema-extractor.d.ts +26 -1
- package/dist/schema-extractor.d.ts.map +1 -1
- package/dist/schema-extractor.js +60 -0
- package/dist/schema-extractor.js.map +1 -1
- package/dist/types.d.ts +33 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +5 -2
- package/src/base.ts +103 -0
- package/src/index.ts +24 -0
- package/src/mcp-client.ts +307 -0
- package/src/mcp-sdk-transport.ts +449 -0
- package/src/schema-extractor.ts +71 -1
- package/src/types.ts +34 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Protocol Client for Photons
|
|
3
|
+
*
|
|
4
|
+
* Enables Photons to call external MCPs via the MCP protocol.
|
|
5
|
+
* This is runtime-agnostic - the actual transport is provided by the runtime (NCP, Lumina, etc.)
|
|
6
|
+
*
|
|
7
|
+
* Usage in Photon:
|
|
8
|
+
* ```typescript
|
|
9
|
+
* export default class MyPhoton extends PhotonMCP {
|
|
10
|
+
* async doSomething() {
|
|
11
|
+
* const github = this.mcp('github');
|
|
12
|
+
* const issues = await github.call('list_issues', { repo: 'foo/bar' });
|
|
13
|
+
* }
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Tool information returned from MCP discovery
|
|
20
|
+
*/
|
|
21
|
+
export interface MCPToolInfo {
|
|
22
|
+
name: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
inputSchema?: {
|
|
25
|
+
type: 'object';
|
|
26
|
+
properties?: Record<string, any>;
|
|
27
|
+
required?: string[];
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Result from an MCP tool call
|
|
33
|
+
*/
|
|
34
|
+
export interface MCPToolResult {
|
|
35
|
+
content: Array<{
|
|
36
|
+
type: 'text' | 'image' | 'resource';
|
|
37
|
+
text?: string;
|
|
38
|
+
data?: string;
|
|
39
|
+
mimeType?: string;
|
|
40
|
+
}>;
|
|
41
|
+
isError?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Interface that runtimes must implement to provide MCP connectivity
|
|
46
|
+
* This keeps photon-core runtime-agnostic
|
|
47
|
+
*/
|
|
48
|
+
export interface MCPTransport {
|
|
49
|
+
/**
|
|
50
|
+
* Call a tool on an MCP server
|
|
51
|
+
* @param mcpName The MCP server name
|
|
52
|
+
* @param toolName The tool to call
|
|
53
|
+
* @param parameters Tool parameters
|
|
54
|
+
*/
|
|
55
|
+
callTool(
|
|
56
|
+
mcpName: string,
|
|
57
|
+
toolName: string,
|
|
58
|
+
parameters: Record<string, any>
|
|
59
|
+
): Promise<MCPToolResult>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* List available tools on an MCP server
|
|
63
|
+
* @param mcpName The MCP server name
|
|
64
|
+
*/
|
|
65
|
+
listTools(mcpName: string): Promise<MCPToolInfo[]>;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if an MCP server is connected/available
|
|
69
|
+
* @param mcpName The MCP server name
|
|
70
|
+
*/
|
|
71
|
+
isConnected(mcpName: string): Promise<boolean>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Factory interface for creating MCP clients
|
|
76
|
+
* Runtimes implement this to provide MCP access to Photons
|
|
77
|
+
*/
|
|
78
|
+
export interface MCPClientFactory {
|
|
79
|
+
/**
|
|
80
|
+
* Create an MCP client for a specific server
|
|
81
|
+
* @param mcpName The MCP server name
|
|
82
|
+
*/
|
|
83
|
+
create(mcpName: string): MCPClient;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* List all available MCP servers
|
|
87
|
+
*/
|
|
88
|
+
listServers(): Promise<string[]>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* MCP Client - Protocol wrapper for calling external MCPs
|
|
93
|
+
*
|
|
94
|
+
* Provides a clean async interface for Photons to call MCP tools.
|
|
95
|
+
* The actual protocol communication is handled by the transport layer.
|
|
96
|
+
*/
|
|
97
|
+
export class MCPClient {
|
|
98
|
+
private toolsCache: MCPToolInfo[] | null = null;
|
|
99
|
+
|
|
100
|
+
constructor(
|
|
101
|
+
private mcpName: string,
|
|
102
|
+
private transport: MCPTransport
|
|
103
|
+
) {}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get the MCP server name
|
|
107
|
+
*/
|
|
108
|
+
get name(): string {
|
|
109
|
+
return this.mcpName;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Call a tool on this MCP server
|
|
114
|
+
*
|
|
115
|
+
* @param toolName The tool to call
|
|
116
|
+
* @param parameters Tool parameters
|
|
117
|
+
* @returns Tool result (parsed from MCP response)
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```typescript
|
|
121
|
+
* const github = this.mcp('github');
|
|
122
|
+
* const issues = await github.call('list_issues', { repo: 'owner/repo', state: 'open' });
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
async call(toolName: string, parameters: Record<string, any> = {}): Promise<any> {
|
|
126
|
+
// Check connection first
|
|
127
|
+
const connected = await this.transport.isConnected(this.mcpName);
|
|
128
|
+
if (!connected) {
|
|
129
|
+
throw new MCPNotConnectedError(this.mcpName);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const result = await this.transport.callTool(this.mcpName, toolName, parameters);
|
|
134
|
+
|
|
135
|
+
if (result.isError) {
|
|
136
|
+
const errorText = result.content.find(c => c.type === 'text')?.text || 'Unknown error';
|
|
137
|
+
throw new MCPToolError(this.mcpName, toolName, errorText);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Extract and parse the result
|
|
141
|
+
return this.parseResult(result);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
if (error instanceof MCPError) {
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
throw new MCPToolError(
|
|
147
|
+
this.mcpName,
|
|
148
|
+
toolName,
|
|
149
|
+
error instanceof Error ? error.message : String(error)
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* List all available tools on this MCP server
|
|
156
|
+
*
|
|
157
|
+
* @returns Array of tool information
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```typescript
|
|
161
|
+
* const github = this.mcp('github');
|
|
162
|
+
* const tools = await github.list();
|
|
163
|
+
* // [{ name: 'list_issues', description: '...' }, ...]
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
async list(): Promise<MCPToolInfo[]> {
|
|
167
|
+
if (this.toolsCache) {
|
|
168
|
+
return this.toolsCache;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const connected = await this.transport.isConnected(this.mcpName);
|
|
172
|
+
if (!connected) {
|
|
173
|
+
throw new MCPNotConnectedError(this.mcpName);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
this.toolsCache = await this.transport.listTools(this.mcpName);
|
|
177
|
+
return this.toolsCache;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Find tools matching a query
|
|
182
|
+
*
|
|
183
|
+
* @param query Search query (matches name or description)
|
|
184
|
+
* @returns Matching tools
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```typescript
|
|
188
|
+
* const github = this.mcp('github');
|
|
189
|
+
* const issueTools = await github.find('issue');
|
|
190
|
+
* ```
|
|
191
|
+
*/
|
|
192
|
+
async find(query: string): Promise<MCPToolInfo[]> {
|
|
193
|
+
const tools = await this.list();
|
|
194
|
+
const lowerQuery = query.toLowerCase();
|
|
195
|
+
return tools.filter(
|
|
196
|
+
t =>
|
|
197
|
+
t.name.toLowerCase().includes(lowerQuery) ||
|
|
198
|
+
t.description?.toLowerCase().includes(lowerQuery)
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Check if this MCP server is connected
|
|
204
|
+
*/
|
|
205
|
+
async isConnected(): Promise<boolean> {
|
|
206
|
+
return this.transport.isConnected(this.mcpName);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Clear the tools cache (useful after reconnection)
|
|
211
|
+
*/
|
|
212
|
+
clearCache(): void {
|
|
213
|
+
this.toolsCache = null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Parse MCP tool result into a usable value
|
|
218
|
+
*/
|
|
219
|
+
private parseResult(result: MCPToolResult): any {
|
|
220
|
+
if (!result.content || result.content.length === 0) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Single text result - try to parse as JSON
|
|
225
|
+
if (result.content.length === 1 && result.content[0].type === 'text') {
|
|
226
|
+
const text = result.content[0].text || '';
|
|
227
|
+
try {
|
|
228
|
+
return JSON.parse(text);
|
|
229
|
+
} catch {
|
|
230
|
+
return text;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Multiple results or non-text - return as-is
|
|
235
|
+
return result.content.map(c => {
|
|
236
|
+
if (c.type === 'text') {
|
|
237
|
+
try {
|
|
238
|
+
return JSON.parse(c.text || '');
|
|
239
|
+
} catch {
|
|
240
|
+
return c.text;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return c;
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Base class for MCP-related errors
|
|
250
|
+
*/
|
|
251
|
+
export class MCPError extends Error {
|
|
252
|
+
constructor(
|
|
253
|
+
public readonly mcpName: string,
|
|
254
|
+
message: string
|
|
255
|
+
) {
|
|
256
|
+
super(message);
|
|
257
|
+
this.name = 'MCPError';
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Error thrown when MCP server is not connected
|
|
263
|
+
*/
|
|
264
|
+
export class MCPNotConnectedError extends MCPError {
|
|
265
|
+
constructor(mcpName: string) {
|
|
266
|
+
super(mcpName, `MCP server '${mcpName}' is not connected`);
|
|
267
|
+
this.name = 'MCPNotConnectedError';
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Error thrown when MCP tool call fails
|
|
273
|
+
*/
|
|
274
|
+
export class MCPToolError extends MCPError {
|
|
275
|
+
constructor(
|
|
276
|
+
mcpName: string,
|
|
277
|
+
public readonly toolName: string,
|
|
278
|
+
public readonly details: string
|
|
279
|
+
) {
|
|
280
|
+
super(mcpName, `MCP tool '${mcpName}:${toolName}' failed: ${details}`);
|
|
281
|
+
this.name = 'MCPToolError';
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Create a proxy-based MCP client that allows direct method calls
|
|
287
|
+
*
|
|
288
|
+
* This enables a more fluent API:
|
|
289
|
+
* ```typescript
|
|
290
|
+
* const github = this.mcp('github');
|
|
291
|
+
* // Instead of: await github.call('list_issues', { repo: 'foo/bar' })
|
|
292
|
+
* // You can do: await github.list_issues({ repo: 'foo/bar' })
|
|
293
|
+
* ```
|
|
294
|
+
*/
|
|
295
|
+
export function createMCPProxy(client: MCPClient): MCPClient & Record<string, (params?: any) => Promise<any>> {
|
|
296
|
+
return new Proxy(client, {
|
|
297
|
+
get(target, prop: string) {
|
|
298
|
+
// Return existing methods
|
|
299
|
+
if (prop in target) {
|
|
300
|
+
return (target as any)[prop];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Return a function that calls the tool
|
|
304
|
+
return (params: Record<string, any> = {}) => target.call(prop, params);
|
|
305
|
+
},
|
|
306
|
+
}) as MCPClient & Record<string, (params?: any) => Promise<any>>;
|
|
307
|
+
}
|