@mcp-shark/mcp-shark 1.4.1 → 1.4.2

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.
Files changed (29) hide show
  1. package/mcp-server/lib/auditor/audit.js +12 -4
  2. package/mcp-server/lib/server/external/kv.js +17 -28
  3. package/mcp-server/lib/server/internal/handlers/prompts-get.js +14 -6
  4. package/mcp-server/lib/server/internal/handlers/prompts-list.js +9 -4
  5. package/mcp-server/lib/server/internal/handlers/resources-list.js +9 -4
  6. package/mcp-server/lib/server/internal/handlers/resources-read.js +13 -3
  7. package/mcp-server/lib/server/internal/handlers/tools-call.js +12 -8
  8. package/mcp-server/lib/server/internal/handlers/tools-list.js +4 -3
  9. package/mcp-server/lib/server/internal/run.js +1 -1
  10. package/mcp-server/lib/server/internal/server.js +10 -10
  11. package/mcp-server/lib/server/internal/session.js +14 -6
  12. package/package.json +1 -1
  13. package/ui/server/routes/composite.js +16 -0
  14. package/ui/server/routes/playground.js +45 -14
  15. package/ui/server/utils/config-update.js +136 -109
  16. package/ui/server.js +1 -0
  17. package/ui/src/components/GroupedByMcpView.jsx +0 -6
  18. package/ui/src/components/McpPlayground/PromptsSection/PromptItem.jsx +46 -21
  19. package/ui/src/components/McpPlayground/PromptsSection/PromptsList.jsx +10 -8
  20. package/ui/src/components/McpPlayground/ResourcesSection/ResourceItem.jsx +60 -32
  21. package/ui/src/components/McpPlayground/ResourcesSection/ResourcesList.jsx +10 -8
  22. package/ui/src/components/McpPlayground/ToolsSection/ToolItem.jsx +46 -21
  23. package/ui/src/components/McpPlayground/ToolsSection/ToolsList.jsx +10 -8
  24. package/ui/src/components/McpPlayground/hooks/useMcpDataLoader.js +107 -0
  25. package/ui/src/components/McpPlayground/hooks/useMcpRequest.js +65 -0
  26. package/ui/src/components/McpPlayground/hooks/useMcpServerStatus.js +70 -0
  27. package/ui/src/components/McpPlayground/useMcpPlayground.js +68 -137
  28. package/ui/src/components/McpPlayground.jsx +100 -21
  29. package/ui/src/utils/requestUtils.js +5 -4
@@ -135,14 +135,22 @@ export async function withAuditRequestResponseHandler(
135
135
  transport,
136
136
  req,
137
137
  res,
138
- auditLogger
138
+ auditLogger,
139
+ requestedMcpServer,
140
+ initialSessionId
139
141
  ) {
140
142
  const reqBuf = await readBody(req);
141
143
  const reqJsonRpc = tryParseJsonRpc(reqBuf);
142
144
 
143
145
  // Extract session ID from request
144
146
  // If no session ID exists, it's an initiation request
145
- const sessionId = getSessionFromRequest(req);
147
+ const sessionIdFromRequest = getSessionFromRequest(req);
148
+ const sessionId =
149
+ sessionIdFromRequest === null ||
150
+ sessionIdFromRequest === undefined ||
151
+ sessionIdFromRequest === ''
152
+ ? initialSessionId
153
+ : sessionIdFromRequest;
146
154
 
147
155
  // Extract request body as string
148
156
  const reqBodyStr = reqBuf.toString('utf8');
@@ -161,7 +169,7 @@ export async function withAuditRequestResponseHandler(
161
169
  headers: req.headers,
162
170
  body: reqBodyJson || reqBodyStr,
163
171
  userAgent: req.headers['user-agent'] || req.headers['User-Agent'] || null,
164
- remoteAddress: req.socket?.remoteAddress || null,
172
+ remoteAddress: requestedMcpServer,
165
173
  sessionId: sessionId || null,
166
174
  });
167
175
 
@@ -223,6 +231,6 @@ export async function withAuditRequestResponseHandler(
223
231
  jsonrpcId,
224
232
  sessionId: sessionId || null,
225
233
  userAgent: req.headers['user-agent'] || req.headers['User-Agent'] || null,
226
- remoteAddress: req.socket?.remoteAddress || null,
234
+ remoteAddress: requestedMcpServer || null,
227
235
  });
228
236
  }
@@ -1,14 +1,5 @@
1
1
  const kv = new Map();
2
2
 
3
- function buildName(name, typeName) {
4
- return `${name}.${typeName}`;
5
- }
6
-
7
- export function extractName(name) {
8
- const [serverName, typeName] = name.split('.');
9
- return { serverName, typeName };
10
- }
11
-
12
3
  export function buildKv(downstreamServers) {
13
4
  for (const downstreamServer of downstreamServers) {
14
5
  const {
@@ -43,16 +34,16 @@ export function buildKv(downstreamServers) {
43
34
  resourcesMap,
44
35
  promptsMap,
45
36
  tools: tools.map(tool => {
46
- return { ...tool, name: buildName(name, tool.name) };
37
+ return { ...tool, name: tool.name };
47
38
  }),
48
39
  resources: resources.map(resource => {
49
40
  return {
50
41
  ...resource,
51
- name: buildName(name, resource.name),
42
+ name: resource.name,
52
43
  };
53
44
  }),
54
45
  prompts: prompts.map(prompt => {
55
- return { ...prompt, name: buildName(name, prompt.name) };
46
+ return { ...prompt, name: prompt.name };
56
47
  }),
57
48
  });
58
49
  }
@@ -61,42 +52,40 @@ export function buildKv(downstreamServers) {
61
52
  return kv;
62
53
  }
63
54
 
64
- export function getBy(database, calledName, action) {
65
- const { serverName, typeName } = extractName(calledName);
66
- if (!serverName || !typeName) {
67
- return null;
68
- }
69
- const entry = database.get(serverName);
55
+ export function getBy(database, requestedMcpServer, calledName, action) {
56
+ const entry = database.get(requestedMcpServer);
70
57
  if (!entry) {
71
58
  return null;
72
59
  }
73
60
 
74
61
  // Type-based lookup
75
62
  if (action === 'getTools') {
76
- return entry.toolsMap.get(typeName);
63
+ return entry.toolsMap.get(calledName);
77
64
  }
78
65
  if (action === 'getResources') {
79
- return entry.resourcesMap.get(typeName);
66
+ return entry.resourcesMap.get(calledName);
80
67
  }
81
68
  if (action === 'getPrompts') {
82
- return entry.promptsMap.get(typeName);
69
+ return entry.promptsMap.get(calledName);
83
70
  }
84
71
 
85
72
  // Action-based lookup
86
73
  if (action === 'callTool') {
87
- return entry.toolsMap.get(typeName);
74
+ return entry.toolsMap.get(calledName);
88
75
  }
89
76
  if (action === 'readResource') {
90
- return entry.resourcesMap.get(typeName);
77
+ return entry.resourcesMap.get(calledName);
91
78
  }
92
79
  if (action === 'getPrompt') {
93
- return entry.promptsMap.get(typeName);
80
+ return entry.promptsMap.get(calledName);
94
81
  }
95
82
  return null;
96
83
  }
97
84
 
98
- export function listAll(database, type) {
99
- return Array.from(database.values())
100
- .map(entry => entry[type])
101
- .flat();
85
+ export function listAll(database, requestedMcpServer, type) {
86
+ const serverEntry = database.get(requestedMcpServer);
87
+ if (!serverEntry) {
88
+ return [];
89
+ }
90
+ return serverEntry[type];
102
91
  }
@@ -1,20 +1,28 @@
1
- import { getBy, extractName } from '../../external/kv.js';
1
+ import { getBy } from '../../external/kv.js';
2
2
  import { InternalServerError } from './error.js';
3
3
 
4
- export function createPromptsGetHandler(logger, mcpServers) {
4
+ export function createPromptsGetHandler(
5
+ logger,
6
+ mcpServers,
7
+ requestedMcpServer
8
+ ) {
5
9
  return async req => {
6
10
  const name = req.params.name;
7
11
  const promptArgs = req?.params?.arguments || {};
8
12
  logger.debug('Prompt get', name, promptArgs);
9
13
 
10
- const { typeName } = extractName(name);
11
-
12
- const getPrompt = getBy(mcpServers, name, 'getPrompt', promptArgs);
14
+ const getPrompt = getBy(
15
+ mcpServers,
16
+ requestedMcpServer,
17
+ name,
18
+ 'getPrompt',
19
+ promptArgs
20
+ );
13
21
  if (!getPrompt) {
14
22
  throw new InternalServerError(`Prompt not found: ${name}`);
15
23
  }
16
24
 
17
- const result = await getPrompt(typeName, promptArgs);
25
+ const result = await getPrompt(name, promptArgs);
18
26
  logger.debug('Prompt get result', result);
19
27
 
20
28
  return result;
@@ -1,10 +1,15 @@
1
1
  import { listAll } from '../../external/kv.js';
2
2
 
3
- export function createPromptsListHandler(logger, mcpServers) {
4
- return async _req => {
5
- logger.debug('Prompts list');
3
+ export function createPromptsListHandler(
4
+ logger,
5
+ mcpServers,
6
+ requestedMcpServer
7
+ ) {
8
+ return async req => {
9
+ const path = req.path;
10
+ logger.debug('Prompts list', path);
6
11
 
7
- const res = await listAll(mcpServers, 'prompts');
12
+ const res = await listAll(mcpServers, requestedMcpServer, 'prompts');
8
13
  const result = Array.isArray(res) ? { prompts: res } : res;
9
14
 
10
15
  return result;
@@ -1,10 +1,15 @@
1
1
  import { listAll } from '../../external/kv.js';
2
2
 
3
- export function createResourcesListHandler(logger, mcpServers) {
4
- return async _req => {
5
- logger.debug('Resources list');
3
+ export function createResourcesListHandler(
4
+ logger,
5
+ mcpServers,
6
+ requestedMcpServer
7
+ ) {
8
+ return async req => {
9
+ const path = req.path;
10
+ logger.debug('Resources list', path);
6
11
 
7
- const res = await listAll(mcpServers, 'resources');
12
+ const res = await listAll(mcpServers, requestedMcpServer, 'resources');
8
13
  const result = Array.isArray(res) ? { resources: res } : res;
9
14
 
10
15
  return result;
@@ -1,12 +1,22 @@
1
1
  import { getBy } from '../../external/kv.js';
2
2
  import { InternalServerError } from './error.js';
3
3
 
4
- export function createResourcesReadHandler(logger, mcpServers) {
4
+ export function createResourcesReadHandler(
5
+ logger,
6
+ mcpServers,
7
+ requestedMcpServer
8
+ ) {
5
9
  return async req => {
10
+ const path = req.path;
6
11
  const uri = req.params.uri;
7
- logger.debug('Resource read', uri);
12
+ logger.debug('Resource read', path, uri);
8
13
 
9
- const readResource = getBy(mcpServers, uri, 'readResource');
14
+ const readResource = getBy(
15
+ mcpServers,
16
+ requestedMcpServer,
17
+ uri,
18
+ 'readResource'
19
+ );
10
20
  if (!readResource) {
11
21
  throw new InternalServerError(`Resource not found: ${uri}`);
12
22
  }
@@ -1,24 +1,28 @@
1
- import { getBy, extractName } from '../../external/kv.js';
1
+ import { getBy } from '../../external/kv.js';
2
2
  import { InternalServerError } from './error.js';
3
3
 
4
4
  const isAsyncIterable = v => v && typeof v[Symbol.asyncIterator] === 'function';
5
5
 
6
- export function createToolsCallHandler(logger, mcpServers) {
6
+ export function createToolsCallHandler(logger, mcpServers, requestedMcpServer) {
7
7
  return async req => {
8
+ const path = req.path;
8
9
  const { name, arguments: args } = req.params;
9
- logger.debug('Tool call', name, args);
10
+ logger.debug('Tool call', path, name, args);
10
11
 
11
- // Extract real server name from concatenated name
12
- const { typeName } = extractName(name);
13
-
14
- const callTool = getBy(mcpServers, name, 'callTool', args || {});
12
+ const callTool = getBy(
13
+ mcpServers,
14
+ requestedMcpServer,
15
+ name,
16
+ 'callTool',
17
+ args || {}
18
+ );
15
19
  if (!callTool) {
16
20
  throw new InternalServerError(`Tool not found: ${name}`);
17
21
  }
18
22
 
19
23
  const result = await callTool({
20
24
  ...req.params,
21
- name: typeName,
25
+ name,
22
26
  });
23
27
  logger.debug('Tool call result', result);
24
28
 
@@ -1,10 +1,11 @@
1
1
  import { listAll } from '../../external/kv.js';
2
2
 
3
- export function createToolsListHandler(logger, mcpServers) {
3
+ export function createToolsListHandler(logger, mcpServers, requestedMcpServer) {
4
4
  return async req => {
5
- logger.debug('Listing tools', req);
5
+ const path = req.path;
6
+ logger.debug('Listing tools', path);
6
7
 
7
- const res = await listAll(mcpServers, 'tools');
8
+ const res = await listAll(mcpServers, requestedMcpServer, 'tools');
8
9
  logger.debug('Tools list result', res);
9
10
 
10
11
  const result = Array.isArray(res) ? { tools: res } : res;
@@ -22,7 +22,7 @@ export function getInternalServer(
22
22
  })
23
23
  );
24
24
 
25
- app.all('/mcp', async (req, res) => {
25
+ app.all('/mcp/*', async (req, res) => {
26
26
  await withSession(
27
27
  serverFactory,
28
28
  withAuditRequestResponseHandler,
@@ -15,49 +15,49 @@ import { createPromptsListHandler } from './handlers/prompts-list.js';
15
15
  import { createPromptsGetHandler } from './handlers/prompts-get.js';
16
16
  import { createResourcesListHandler } from './handlers/resources-list.js';
17
17
  import { createResourcesReadHandler } from './handlers/resources-read.js';
18
- import { SERVER_NAME } from './handlers/common.js';
19
18
 
20
- export function createInternalServer(logger, mcpServers) {
19
+ export function createInternalServer(logger, mcpServers, requestedMcpServer) {
21
20
  // create MCP server
22
21
  const server = new Server(
23
- { name: SERVER_NAME, version: '1.0.0' },
22
+ { name: requestedMcpServer, version: '1.0.0' },
24
23
  { capabilities: { tools: {}, prompts: {}, resources: {} } }
25
24
  );
26
25
 
27
26
  // Register handlers
28
27
  server.setRequestHandler(
29
28
  ListToolsRequestSchema,
30
- createToolsListHandler(logger, mcpServers)
29
+ createToolsListHandler(logger, mcpServers, requestedMcpServer)
31
30
  );
32
31
 
33
32
  server.setRequestHandler(
34
33
  CallToolRequestSchema,
35
- createToolsCallHandler(logger, mcpServers)
34
+ createToolsCallHandler(logger, mcpServers, requestedMcpServer)
36
35
  );
37
36
 
38
37
  server.setRequestHandler(
39
38
  ListPromptsRequestSchema,
40
- createPromptsListHandler(logger, mcpServers)
39
+ createPromptsListHandler(logger, mcpServers, requestedMcpServer)
41
40
  );
42
41
 
43
42
  server.setRequestHandler(
44
43
  GetPromptRequestSchema,
45
- createPromptsGetHandler(logger, mcpServers)
44
+ createPromptsGetHandler(logger, mcpServers, requestedMcpServer)
46
45
  );
47
46
 
48
47
  server.setRequestHandler(
49
48
  ListResourcesRequestSchema,
50
- createResourcesListHandler(logger, mcpServers)
49
+ createResourcesListHandler(logger, mcpServers, requestedMcpServer)
51
50
  );
52
51
 
53
52
  server.setRequestHandler(
54
53
  ReadResourceRequestSchema,
55
- createResourcesReadHandler(logger, mcpServers)
54
+ createResourcesReadHandler(logger, mcpServers, requestedMcpServer)
56
55
  );
57
56
 
58
57
  return server;
59
58
  }
60
59
 
61
60
  export function createInternalServerFactory(logger, mcpServers) {
62
- return () => createInternalServer(logger, mcpServers);
61
+ return requestedMcpServer =>
62
+ createInternalServer(logger, mcpServers, requestedMcpServer);
63
63
  }
@@ -20,20 +20,28 @@ export async function withSession(
20
20
  res,
21
21
  auditLogger
22
22
  ) {
23
+ const requestedMcpServer = req.params[0];
23
24
  const sessionId = getSessionFromRequest(req);
24
25
  if (!sessionId) {
25
- const newSessionId = randomUUID();
26
+ const initialSessionId = randomUUID();
26
27
  const transport = new StreamableHTTPServerTransport({
27
- sessionIdGenerator: () => newSessionId,
28
+ sessionIdGenerator: () => initialSessionId,
28
29
  enableJsonResponse: true,
29
30
  });
30
- const server = serverFactory();
31
+ const server = serverFactory(requestedMcpServer);
31
32
  await server.connect(transport);
32
- storeTransportInSession(newSessionId, transport);
33
+ storeTransportInSession(initialSessionId, transport);
33
34
  // Session creation will be logged as part of the request packet in audit.js
34
- return requestHandler(transport, req, res, auditLogger);
35
+ return requestHandler(
36
+ transport,
37
+ req,
38
+ res,
39
+ auditLogger,
40
+ requestedMcpServer,
41
+ initialSessionId
42
+ );
35
43
  }
36
44
 
37
45
  const transport = getTransportFromSession(sessionId);
38
- return requestHandler(transport, req, res, auditLogger);
46
+ return requestHandler(transport, req, res, auditLogger, requestedMcpServer);
39
47
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-shark/mcp-shark",
3
- "version": "1.4.1",
3
+ "version": "1.4.2",
4
4
  "description": "Aggregate multiple Model Context Protocol (MCP) servers into a single unified interface with a powerful monitoring UI. Prov deep visibility into every request and response.",
5
5
  "type": "module",
6
6
  "main": "./bin/mcp-shark.js",
@@ -240,5 +240,21 @@ export function createCompositeRoutes(
240
240
  });
241
241
  };
242
242
 
243
+ router.getServers = (req, res) => {
244
+ try {
245
+ const mcpsJsonPath = getMcpConfigPath();
246
+ if (!fs.existsSync(mcpsJsonPath)) {
247
+ return res.json({ servers: [] });
248
+ }
249
+
250
+ const configContent = fs.readFileSync(mcpsJsonPath, 'utf-8');
251
+ const config = JSON.parse(configContent);
252
+ const servers = config.servers ? Object.keys(config.servers) : [];
253
+ res.json({ servers });
254
+ } catch (error) {
255
+ res.status(500).json({ error: 'Failed to get servers', details: error.message });
256
+ }
257
+ };
258
+
243
259
  return router;
244
260
  }
@@ -1,20 +1,31 @@
1
1
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
2
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
3
3
 
4
- const MCP_SERVER_URL = 'http://localhost:9851/mcp';
4
+ const MCP_SERVER_BASE_URL = 'http://localhost:9851/mcp';
5
5
 
6
- // Store client connections per session
6
+ // Store client connections per server and session
7
7
  const clientSessions = new Map();
8
8
 
9
+ function getSessionKey(serverName, sessionId) {
10
+ return `${serverName}:${sessionId}`;
11
+ }
12
+
9
13
  export function createPlaygroundRoutes() {
10
14
  const router = {};
11
15
 
12
- // Get or create client for a session
13
- async function getClient(sessionId) {
14
- if (clientSessions.has(sessionId)) {
15
- return clientSessions.get(sessionId);
16
+ // Get or create client for a session and server
17
+ async function getClient(serverName, sessionId) {
18
+ const sessionKey = getSessionKey(serverName, sessionId);
19
+ if (clientSessions.has(sessionKey)) {
20
+ return clientSessions.get(sessionKey);
21
+ }
22
+
23
+ if (!serverName) {
24
+ throw new Error('Server name is required');
16
25
  }
17
26
 
27
+ const mcpServerUrl = `${MCP_SERVER_BASE_URL}/${encodeURIComponent(serverName)}`;
28
+
18
29
  const client = new Client(
19
30
  { name: 'mcp-shark-playground', version: '1.0.0' },
20
31
  {
@@ -26,7 +37,7 @@ export function createPlaygroundRoutes() {
26
37
  }
27
38
  );
28
39
 
29
- const transport = new StreamableHTTPClientTransport(new URL(MCP_SERVER_URL));
40
+ const transport = new StreamableHTTPClientTransport(new URL(mcpServerUrl));
30
41
  await client.connect(transport);
31
42
 
32
43
  const clientWrapper = {
@@ -38,13 +49,13 @@ export function createPlaygroundRoutes() {
38
49
  },
39
50
  };
40
51
 
41
- clientSessions.set(sessionId, clientWrapper);
52
+ clientSessions.set(sessionKey, clientWrapper);
42
53
  return clientWrapper;
43
54
  }
44
55
 
45
56
  router.proxyRequest = async (req, res) => {
46
57
  try {
47
- const { method, params } = req.body;
58
+ const { method, params, serverName } = req.body;
48
59
 
49
60
  if (!method) {
50
61
  return res.status(400).json({
@@ -53,13 +64,20 @@ export function createPlaygroundRoutes() {
53
64
  });
54
65
  }
55
66
 
67
+ if (!serverName) {
68
+ return res.status(400).json({
69
+ error: 'Invalid request',
70
+ message: 'serverName field is required',
71
+ });
72
+ }
73
+
56
74
  // Get or create session ID
57
75
  const sessionId =
58
76
  req.headers['mcp-session-id'] ||
59
77
  req.headers['x-mcp-session-id'] ||
60
78
  `playground-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
61
79
 
62
- const { client } = await getClient(sessionId);
80
+ const { client } = await getClient(serverName, sessionId);
63
81
 
64
82
  let result;
65
83
  switch (method) {
@@ -140,10 +158,23 @@ export function createPlaygroundRoutes() {
140
158
  // Cleanup endpoint to close client connections
141
159
  router.cleanup = async (req, res) => {
142
160
  const sessionId = req.headers['mcp-session-id'] || req.headers['x-mcp-session-id'];
143
- if (sessionId && clientSessions.has(sessionId)) {
144
- const clientWrapper = clientSessions.get(sessionId);
145
- await clientWrapper.close();
146
- clientSessions.delete(sessionId);
161
+ const { serverName } = req.body || {};
162
+
163
+ if (serverName && sessionId) {
164
+ const sessionKey = getSessionKey(serverName, sessionId);
165
+ if (clientSessions.has(sessionKey)) {
166
+ const clientWrapper = clientSessions.get(sessionKey);
167
+ await clientWrapper.close();
168
+ clientSessions.delete(sessionKey);
169
+ }
170
+ } else if (sessionId) {
171
+ // Cleanup all sessions for this sessionId across all servers
172
+ for (const [key, clientWrapper] of clientSessions.entries()) {
173
+ if (key.endsWith(`:${sessionId}`)) {
174
+ await clientWrapper.close();
175
+ clientSessions.delete(key);
176
+ }
177
+ }
147
178
  }
148
179
  res.json({ success: true });
149
180
  };