v2 ESlint fixes (#1403)
Co-authored-by: Matt <77928207+mattzcarey@users.noreply.github.com>
This commit is contained in:
parent
9296459ac0
commit
9fc7e28f48
1 changed files with 1672 additions and 1346 deletions
|
|
@ -41,8 +41,8 @@ let notificationCount = 0;
|
|||
let client: Client | null = null;
|
||||
let transport: StreamableHTTPClientTransport | null = null;
|
||||
let serverUrl = 'http://localhost:3000/mcp';
|
||||
let notificationsToolLastEventId: string | undefined = undefined;
|
||||
let sessionId: string | undefined = undefined;
|
||||
let notificationsToolLastEventId: string | undefined;
|
||||
let sessionId: string | undefined;
|
||||
|
||||
async function main(): Promise<void> {
|
||||
console.log('MCP Interactive Client');
|
||||
|
|
@ -85,27 +85,32 @@ function commandLoop(): void {
|
|||
|
||||
try {
|
||||
switch (command) {
|
||||
case 'connect':
|
||||
case 'connect': {
|
||||
await connect(args[1]);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'disconnect':
|
||||
case 'disconnect': {
|
||||
await disconnect();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'terminate-session':
|
||||
case 'terminate-session': {
|
||||
await terminateSession();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'reconnect':
|
||||
case 'reconnect': {
|
||||
await reconnect();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'list-tools':
|
||||
case 'list-tools': {
|
||||
await listTools();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'call-tool':
|
||||
case 'call-tool': {
|
||||
if (args.length < 2) {
|
||||
console.log('Usage: call-tool <name> [args]');
|
||||
} else {
|
||||
|
|
@ -121,34 +126,38 @@ function commandLoop(): void {
|
|||
await callTool(toolName, toolArgs);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'greet':
|
||||
case 'greet': {
|
||||
await callGreetTool(args[1] || 'MCP User');
|
||||
break;
|
||||
}
|
||||
|
||||
case 'multi-greet':
|
||||
case 'multi-greet': {
|
||||
await callMultiGreetTool(args[1] || 'MCP User');
|
||||
break;
|
||||
}
|
||||
|
||||
case 'collect-info':
|
||||
case 'collect-info': {
|
||||
await callCollectInfoTool(args[1] || 'contact');
|
||||
break;
|
||||
}
|
||||
|
||||
case 'start-notifications': {
|
||||
const interval = args[1] ? parseInt(args[1], 10) : 2000;
|
||||
const count = args[2] ? parseInt(args[2], 10) : 10;
|
||||
const interval = args[1] ? Number.parseInt(args[1], 10) : 2000;
|
||||
const count = args[2] ? Number.parseInt(args[2], 10) : 10;
|
||||
await startNotifications(interval, count);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'run-notifications-tool-with-resumability': {
|
||||
const interval = args[1] ? parseInt(args[1], 10) : 2000;
|
||||
const count = args[2] ? parseInt(args[2], 10) : 10;
|
||||
const interval = args[1] ? Number.parseInt(args[1], 10) : 2000;
|
||||
const count = args[2] ? Number.parseInt(args[2], 10) : 10;
|
||||
await runNotificationsToolWithResumability(interval, count);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'call-tool-task':
|
||||
case 'call-tool-task': {
|
||||
if (args.length < 2) {
|
||||
console.log('Usage: call-tool-task <name> [args]');
|
||||
} else {
|
||||
|
|
@ -164,12 +173,14 @@ function commandLoop(): void {
|
|||
await callToolTask(toolName, toolArgs);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'list-prompts':
|
||||
case 'list-prompts': {
|
||||
await listPrompts();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'get-prompt':
|
||||
case 'get-prompt': {
|
||||
if (args.length < 2) {
|
||||
console.log('Usage: get-prompt <name> [args]');
|
||||
} else {
|
||||
|
|
@ -185,33 +196,39 @@ function commandLoop(): void {
|
|||
await getPrompt(promptName, promptArgs);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'list-resources':
|
||||
case 'list-resources': {
|
||||
await listResources();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'read-resource':
|
||||
case 'read-resource': {
|
||||
if (args.length < 2) {
|
||||
console.log('Usage: read-resource <uri>');
|
||||
} else {
|
||||
await readResource(args[1]!);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'help':
|
||||
case 'help': {
|
||||
printHelp();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'quit':
|
||||
case 'exit':
|
||||
case 'exit': {
|
||||
await cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
default:
|
||||
default: {
|
||||
if (command) {
|
||||
console.log(`Unknown command: ${command}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error executing command: ${error}`);
|
||||
|
|
@ -250,7 +267,7 @@ async function connect(url?: string): Promise<void> {
|
|||
}
|
||||
);
|
||||
client.onerror = error => {
|
||||
console.error('\x1b[31mClient error:', error, '\x1b[0m');
|
||||
console.error('\u001B[31mClient error:', error, '\u001B[0m');
|
||||
};
|
||||
|
||||
// Set up elicitation request handler with proper validation
|
||||
|
|
@ -353,25 +370,38 @@ async function connect(url?: string): Promise<void> {
|
|||
// Parse the value based on type
|
||||
let parsedValue: unknown;
|
||||
|
||||
if (field.type === 'boolean') {
|
||||
parsedValue = answer.toLowerCase() === 'true' || answer.toLowerCase() === 'yes' || answer === '1';
|
||||
} else if (field.type === 'number') {
|
||||
parsedValue = parseFloat(answer);
|
||||
if (isNaN(parsedValue as number)) {
|
||||
throw new Error(`${fieldName} must be a valid number`);
|
||||
switch (field.type) {
|
||||
case 'boolean': {
|
||||
parsedValue = answer.toLowerCase() === 'true' || answer.toLowerCase() === 'yes' || answer === '1';
|
||||
|
||||
break;
|
||||
}
|
||||
} else if (field.type === 'integer') {
|
||||
parsedValue = parseInt(answer, 10);
|
||||
if (isNaN(parsedValue as number)) {
|
||||
throw new Error(`${fieldName} must be a valid integer`);
|
||||
case 'number': {
|
||||
parsedValue = Number.parseFloat(answer);
|
||||
if (Number.isNaN(parsedValue as number)) {
|
||||
throw new TypeError(`${fieldName} must be a valid number`);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
} else if (field.enum) {
|
||||
if (!field.enum.includes(answer)) {
|
||||
throw new Error(`${fieldName} must be one of: ${field.enum.join(', ')}`);
|
||||
case 'integer': {
|
||||
parsedValue = Number.parseInt(answer, 10);
|
||||
if (Number.isNaN(parsedValue as number)) {
|
||||
throw new TypeError(`${fieldName} must be a valid integer`);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (field.enum) {
|
||||
if (!field.enum.includes(answer)) {
|
||||
throw new Error(`${fieldName} must be one of: ${field.enum.join(', ')}`);
|
||||
}
|
||||
parsedValue = answer;
|
||||
} else {
|
||||
parsedValue = answer;
|
||||
}
|
||||
}
|
||||
parsedValue = answer;
|
||||
} else {
|
||||
parsedValue = answer;
|
||||
}
|
||||
|
||||
content[fieldName] = parsedValue;
|
||||
|
|
@ -406,9 +436,10 @@ async function connect(url?: string): Promise<void> {
|
|||
|
||||
if (!isValid) {
|
||||
console.log('❌ Validation errors:');
|
||||
validate.errors?.forEach(error => {
|
||||
console.log(` - ${error.instancePath || 'root'}: ${error.message}`);
|
||||
});
|
||||
if (validate.errors)
|
||||
for (const error of validate.errors) {
|
||||
console.log(` - ${error.instancePath || 'root'}: ${error.message}`);
|
||||
}
|
||||
|
||||
if (attempts < maxAttempts) {
|
||||
console.log('Please correct the errors and try again...');
|
||||
|
|
@ -429,20 +460,30 @@ async function connect(url?: string): Promise<void> {
|
|||
});
|
||||
});
|
||||
|
||||
if (confirmAnswer === 'yes' || confirmAnswer === 'y') {
|
||||
return {
|
||||
action: 'accept',
|
||||
content
|
||||
};
|
||||
} else if (confirmAnswer === 'cancel' || confirmAnswer === 'c') {
|
||||
return { action: 'cancel' };
|
||||
} else if (confirmAnswer === 'no' || confirmAnswer === 'n') {
|
||||
if (attempts < maxAttempts) {
|
||||
console.log('Please re-enter the information...');
|
||||
continue;
|
||||
} else {
|
||||
return { action: 'decline' };
|
||||
switch (confirmAnswer) {
|
||||
case 'yes':
|
||||
case 'y': {
|
||||
return {
|
||||
action: 'accept',
|
||||
content
|
||||
};
|
||||
}
|
||||
case 'cancel':
|
||||
case 'c': {
|
||||
return { action: 'cancel' };
|
||||
}
|
||||
case 'no':
|
||||
case 'n': {
|
||||
if (attempts < maxAttempts) {
|
||||
console.log('Please re-enter the information...');
|
||||
continue;
|
||||
} else {
|
||||
return { action: 'decline' };
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
// No default
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -524,7 +565,10 @@ async function terminateSession(): Promise<void> {
|
|||
console.log('Session terminated successfully');
|
||||
|
||||
// Check if sessionId was cleared after termination
|
||||
if (!transport.sessionId) {
|
||||
if (transport.sessionId) {
|
||||
console.log('Server responded with 405 Method Not Allowed (session termination not supported)');
|
||||
console.log('Session ID is still active:', transport.sessionId);
|
||||
} else {
|
||||
console.log('Session ID has been cleared');
|
||||
sessionId = undefined;
|
||||
|
||||
|
|
@ -533,9 +577,6 @@ async function terminateSession(): Promise<void> {
|
|||
console.log('Transport closed after session termination');
|
||||
client = null;
|
||||
transport = null;
|
||||
} else {
|
||||
console.log('Server responded with 405 Method Not Allowed (session termination not supported)');
|
||||
console.log('Session ID is still active:', transport.sessionId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error terminating session:', error);
|
||||
|
|
@ -596,30 +637,47 @@ async function callTool(name: string, args: Record<string, unknown>): Promise<vo
|
|||
console.log('Tool result:');
|
||||
const resourceLinks: ResourceLink[] = [];
|
||||
|
||||
result.content.forEach(item => {
|
||||
if (item.type === 'text') {
|
||||
console.log(` ${item.text}`);
|
||||
} else if (item.type === 'resource_link') {
|
||||
const resourceLink = item as ResourceLink;
|
||||
resourceLinks.push(resourceLink);
|
||||
console.log(` 📁 Resource Link: ${resourceLink.name}`);
|
||||
console.log(` URI: ${resourceLink.uri}`);
|
||||
if (resourceLink.mimeType) {
|
||||
console.log(` Type: ${resourceLink.mimeType}`);
|
||||
for (const item of result.content) {
|
||||
switch (item.type) {
|
||||
case 'text': {
|
||||
console.log(` ${item.text}`);
|
||||
|
||||
break;
|
||||
}
|
||||
if (resourceLink.description) {
|
||||
console.log(` Description: ${resourceLink.description}`);
|
||||
case 'resource_link': {
|
||||
const resourceLink = item as ResourceLink;
|
||||
resourceLinks.push(resourceLink);
|
||||
console.log(` 📁 Resource Link: ${resourceLink.name}`);
|
||||
console.log(` URI: ${resourceLink.uri}`);
|
||||
if (resourceLink.mimeType) {
|
||||
console.log(` Type: ${resourceLink.mimeType}`);
|
||||
}
|
||||
if (resourceLink.description) {
|
||||
console.log(` Description: ${resourceLink.description}`);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'resource': {
|
||||
console.log(` [Embedded Resource: ${item.resource.uri}]`);
|
||||
|
||||
break;
|
||||
}
|
||||
case 'image': {
|
||||
console.log(` [Image: ${item.mimeType}]`);
|
||||
|
||||
break;
|
||||
}
|
||||
case 'audio': {
|
||||
console.log(` [Audio: ${item.mimeType}]`);
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
console.log(` [Unknown content type]:`, item);
|
||||
}
|
||||
} else if (item.type === 'resource') {
|
||||
console.log(` [Embedded Resource: ${item.resource.uri}]`);
|
||||
} else if (item.type === 'image') {
|
||||
console.log(` [Image: ${item.mimeType}]`);
|
||||
} else if (item.type === 'audio') {
|
||||
console.log(` [Audio: ${item.mimeType}]`);
|
||||
} else {
|
||||
console.log(` [Unknown content type]:`, item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Offer to read resource links
|
||||
if (resourceLinks.length > 0) {
|
||||
|
|
@ -678,13 +736,13 @@ async function runNotificationsToolWithResumability(interval: number, count: num
|
|||
});
|
||||
|
||||
console.log('Tool result:');
|
||||
result.content.forEach(item => {
|
||||
for (const item of result.content) {
|
||||
if (item.type === 'text') {
|
||||
console.log(` ${item.text}`);
|
||||
} else {
|
||||
console.log(` ${item.type} content:`, item);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Error starting notification stream: ${error}`);
|
||||
}
|
||||
|
|
@ -732,9 +790,9 @@ async function getPrompt(name: string, args: Record<string, unknown>): Promise<v
|
|||
|
||||
const promptResult = await client.request(promptRequest, GetPromptResultSchema);
|
||||
console.log('Prompt template:');
|
||||
promptResult.messages.forEach((msg, index) => {
|
||||
for (const [index, msg] of promptResult.messages.entries()) {
|
||||
console.log(` [${index + 1}] ${msg.role}: ${msg.content.type === 'text' ? msg.content.text : JSON.stringify(msg.content)}`);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Error getting prompt ${name}: ${error}`);
|
||||
}
|
||||
|
|
@ -830,7 +888,7 @@ async function callToolTask(name: string, args: Record<string, unknown>): Promis
|
|||
CallToolResultSchema,
|
||||
{
|
||||
task: {
|
||||
ttl: 60000 // Keep results for 60 seconds
|
||||
ttl: 60_000 // Keep results for 60 seconds
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
@ -840,26 +898,30 @@ async function callToolTask(name: string, args: Record<string, unknown>): Promis
|
|||
let lastStatus = '';
|
||||
for await (const message of stream) {
|
||||
switch (message.type) {
|
||||
case 'taskCreated':
|
||||
case 'taskCreated': {
|
||||
console.log('Task created successfully with ID:', message.task.taskId);
|
||||
break;
|
||||
case 'taskStatus':
|
||||
}
|
||||
case 'taskStatus': {
|
||||
if (lastStatus !== message.task.status) {
|
||||
console.log(` ${message.task.status}${message.task.statusMessage ? ` - ${message.task.statusMessage}` : ''}`);
|
||||
}
|
||||
lastStatus = message.task.status;
|
||||
break;
|
||||
case 'result':
|
||||
}
|
||||
case 'result': {
|
||||
console.log('Task completed!');
|
||||
console.log('Tool result:');
|
||||
message.result.content.forEach(item => {
|
||||
for (const item of message.result.content) {
|
||||
if (item.type === 'text') {
|
||||
console.log(` ${item.text}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'error':
|
||||
}
|
||||
case 'error': {
|
||||
throw message.error;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -891,6 +953,7 @@ async function cleanup(): Promise<void> {
|
|||
process.stdin.setRawMode(false);
|
||||
readline.close();
|
||||
console.log('\nGoodbye!');
|
||||
// eslint-disable-next-line unicorn/no-process-exit
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
|
|
@ -921,7 +984,10 @@ process.on('SIGINT', async () => {
|
|||
});
|
||||
|
||||
// Start the interactive client
|
||||
main().catch((error: unknown) => {
|
||||
try {
|
||||
await main();
|
||||
} catch (error) {
|
||||
console.error('Error running MCP client:', error);
|
||||
// eslint-disable-next-line unicorn/no-process-exit
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue