Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implementation of the first version #2

Merged
merged 33 commits into from
Jan 31, 2025
Merged
Changes from 1 commit
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
bf27f11
Initial import
jirispilka Jan 27, 2025
45cfdc9
Cleanup code
jirispilka Jan 27, 2025
1cad71d
Working example
jirispilka Jan 27, 2025
e537a68
Improve message handling
jirispilka Jan 27, 2025
c681a92
Lint fix
jirispilka Jan 27, 2025
2d62826
Working solution with SSE
jirispilka Jan 28, 2025
6fe89f8
Process messages
jirispilka Jan 28, 2025
ae3f244
Add README.md
jirispilka Jan 28, 2025
c1b7476
Fix input schema
jirispilka Jan 28, 2025
e2034ac
Merge pull request #1 from apify/detached
jirispilka Jan 28, 2025
16406ce
Update const.ts and input_schema.json
jirispilka Jan 28, 2025
7d1c295
Merge remote-tracking branch 'origin/feat/implementation' into feat/i…
jirispilka Jan 28, 2025
a58ccb9
Add Dockerfile
jirispilka Jan 28, 2025
063c485
Update README.md
jirispilka Jan 28, 2025
18425bc
Update README.md
jirispilka Jan 28, 2025
edeb6cd
Update README.md
jirispilka Jan 29, 2025
c4741df
Update Dockerfile
jirispilka Jan 29, 2025
25f1cdf
Add const
jirispilka Jan 29, 2025
083b133
Fix path
jirispilka Jan 29, 2025
22e4ff0
Fix input
jirispilka Jan 29, 2025
f60a062
Make sure that provided URL is correct
jirispilka Jan 29, 2025
799678e
Add delete conversation
jirispilka Jan 30, 2025
05df1d2
Added error handling
jirispilka Jan 30, 2025
df1b91f
Reorg client.js
jirispilka Jan 30, 2025
7e1d167
Fix
jirispilka Jan 30, 2025
9ad6ff0
Fix
jirispilka Jan 30, 2025
29dd1e0
Fix
jirispilka Jan 30, 2025
4933cf2
Add logging
jirispilka Jan 30, 2025
3f3ebb1
Update README.md
jirispilka Jan 30, 2025
d267810
Add logging
jirispilka Jan 30, 2025
5b66e4d
Comment out the badge
jirispilka Jan 31, 2025
eaad24f
Wait longer on reconnecting
jirispilka Jan 31, 2025
ca1cc7a
Fix readme, remove async from processInput function
jirispilka Jan 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Added error handling
  • Loading branch information
jirispilka committed Jan 30, 2025

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
commit 05df1d2ca6eb9c56c656557b6d9b789900e911c7
6 changes: 6 additions & 0 deletions src/const.ts
Original file line number Diff line number Diff line change
@@ -8,3 +8,9 @@ export const defaults = {
};

export const MISSING_PARAMETER_ERROR = `Either provide parameter as Actor input or as query parameter: `;

export const BASIC_INFORMATION = 'Once you have the Tester MCP Client running, you can ask:\n'
+ '- "What Apify Actors I can use"\n'
+ '- "Which Actor is the best for scraping Instagram comments"\n'
+ "- \"Can you scrape the first 10 pages of Google search results for 'best restaurants in Prague'?\"\n"
+ '\n';
36 changes: 34 additions & 2 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@ import cors from 'cors';
import dotenv from 'dotenv';
import express from 'express';

import { BASIC_INFORMATION } from './const.js';
import { processInput } from './input.js';
import { log } from './logger.js';
import { MCPClient } from './mcpClient.js';
@@ -109,16 +110,47 @@ app.post('/message', async (req, res) => {
}
});

/**
* Periodically check if the main server is still reachable.
*/
app.get('/pingMcpServer', async (_req, res) => {
try {
// Attempt to ping the main MCP server
const response = await client.isConnected();
res.json({ status: response });
} catch (err) {
res.json({ status: 'Not connected', error: (err as Error).message });
}
});

app.post('/reconnect', async (_req, res) => {
try {
log.debug('Reconnecting to main server');
await client.connectToServer();
const response = await client.isConnected();
res.json({ status: response });
} catch (err) {
log.error(`Error reconnecting to main server: ${err}`);
res.json({ status: 'Not connected', error: (err as Error).message });
}
});

/**
* GET /client-info endpoint to provide the client with necessary information
*/
app.get('/client-info', (_req, res) => {
// If you have these values in config or environment, adapt as needed.
res.json({
mcpSseUrl: input.mcpSseUrl,
systemPrompt: input.systemPrompt,
modelName: input.modelName,
publicUrl: publicUrl,
publicUrl,
information: BASIC_INFORMATION,
});
});

/**
* POST /conversation/reset to reset the conversation
*/
app.post('/conversation/reset', (_req, res) => {
client.resetConversation();
res.json({ ok: true });
17 changes: 13 additions & 4 deletions src/mcpClient.ts
Original file line number Diff line number Diff line change
@@ -64,7 +64,7 @@ export class MCPClient {
* Connect to the server using stdio transport and list available tools.
*/
async connectToServer() {
if (this.isConnected) return;
if (this._isConnected) return;
const { customHeaders } = this;
const transport = new SSEClientTransport(
new URL(this.serverUrl),
@@ -84,11 +84,20 @@ export class MCPClient {
);
await this.client.connect(transport);
await this.updateTools();
this._isConnected = true;
}

get isConnected() {
return this._isConnected;
async isConnected() {
try {
await this.client.ping();
this._isConnected = true;
return 'OK';
} catch (error) {
this._isConnected = false;
if (error instanceof Error) {
return error.message;
}
return String(error);
}
}

resetConversation() {
73 changes: 70 additions & 3 deletions src/public/client.js
Original file line number Diff line number Diff line change
@@ -3,10 +3,16 @@

// ===== DOM Elements =====
const chatLog = document.getElementById('chatLog');
const clearBtn = document.getElementById('clearBtn');
const clientInfo = document.getElementById('clientInfo');
const information = document.getElementById('information');
const mcpServerStatus = document.getElementById('mcpServerStatus');
const mcpSseUrl = document.getElementById('mcpSseUrl');
const queryInput = document.getElementById('queryInput');
const reconnectBtn = document.getElementById('reconnectBtn');
const sendBtn = document.getElementById('sendBtn');
const clearBtn = document.getElementById('clearBtn');
const spinner = document.getElementById('spinner');
const statusIcon = document.getElementById('statusIcon');

// Keep local messages for display (optional)
const messages = [];
@@ -31,6 +37,11 @@ eventSource.onerror = (err) => {
appendMessage('internal', `SSE connection error: ${JSON.stringify(err.message) || err}`);
};

// Then call connectSSE as soon as the browser is ready:
document.addEventListener('DOMContentLoaded', () => {
reconnect();
});

/**
* 2) Main function to append messages to the chat.
* If content is an array with tool blocks, we separate them out.
@@ -213,15 +224,17 @@ document.addEventListener('DOMContentLoaded', async () => {
const resp = await fetch('/client-info');
const data = await resp.json();

const mcpSseUrl = document.getElementById('mcpSseUrl');
if (mcpSseUrl) {
mcpSseUrl.textContent = data.mcpSseUrl;
}
// Show the system prompt in a collapsible <details> (assuming you have <pre id="systemPromptPre">)
const clientInfo = document.getElementById('clientInfo');
if (clientInfo) {
clientInfo.textContent = `Model name: ${data.modelName}\nSystem prompt: ${data.systemPrompt}`;
}

if (information) {
information.innerHTML = `${data.information}`;
}
} catch (err) {
console.error('Error fetching client info:', err);
}
@@ -243,3 +256,57 @@ clearBtn.addEventListener('click', async () => {
console.error('Error resetting conversation:', err);
}
});

async function checkConnection() {
fetch('/pingMcpServer')
.then((resp) => resp.json())
.then((data) => {
if (mcpServerStatus) {
updateMcpServerStatus(data.status);
}
})
.catch((err) => {
console.error('Network error calling /pingMcpServer:', err);
if (mcpServerStatus) {
mcpServerStatus.textContent = 'Network error';
}
});
}

async function reconnect() {
fetch('/reconnect', { method: 'POST' })
.then((resp) => resp.json())
.then((data) => {
if (mcpServerStatus) {
updateMcpServerStatus(data.status);
}
})
.catch((err) => {
console.error('Network error calling /reconnect:', err);
if (mcpServerStatus) {
updateMcpServerStatus('Network error');
}
});
}

function updateMcpServerStatus(status) {
const isOk = status === true || status === 'OK';
if (isOk) {
statusIcon.style.backgroundColor = 'green';
mcpServerStatus.textContent = 'OK';
} else {
statusIcon.style.backgroundColor = 'red';
mcpServerStatus.textContent = 'Disconnected';
}
}

reconnectBtn.addEventListener('click', async () => {
mcpServerStatus.textContent = 'Reconnecting...';
/* eslint-disable-next-line no-promise-executor-return */
await new Promise((resolve) => setTimeout(resolve, 500));
await reconnect();
});

setInterval(async () => {
await checkConnection();
}, 5000);
35 changes: 29 additions & 6 deletions src/public/index.html
Original file line number Diff line number Diff line change
@@ -110,16 +110,39 @@
</head>
<body>
<div id="chatContainer">
<div>
<strong>MCP Server URL <span id="mcpSseUrl" style="display: inline;"></span></strong>
</div>
<h2>Actors MCP Client</h2>
<details style="margin: 8px 0;">
<summary>Information</summary>
<summary>Client settings (model name, system prompt)</summary>
<pre id="clientInfo"></pre>
</details>
<h2>Actors MCP Client</h2>
<details style="margin: 8px 0;">
<summary>Information</summary>
<pre id="information"></pre>
</details>
<div>
MCP Server SSE URL: <span id="mcpSseUrl" style="display: inline;"></span>
</div>
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;">
<!-- Left side: Status + Reconnect -->
<div style="display: flex; align-items: center; gap: 8px;">
<span>MCP Server Status:</span>
<!-- Circle icon for status: green or red -->
<span id="statusIcon"
style="
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
background-color: gray;
">
</span>
<span id="mcpServerStatus">Unknown</span>
<button id="reconnectBtn">Reconnect</button>
</div>
<!-- Right side: Delete Conversation -->
<button id="clearBtn">Delete Conversation</button>
</div>
<div id="chatLog"></div>

<div id="inputRow">
<label for="queryInput"></label><textarea id="queryInput" rows="2" placeholder="Type message..."></textarea>
<button id="sendBtn">Send</button>