v2 ESlint fixes (#1403)

Co-authored-by: Matt <77928207+mattzcarey@users.noreply.github.com>
This commit is contained in:
Konstantin Konstantinov 2026-01-20 14:21:53 +02:00 • committed by GitHub
commit 9fc7e28f48
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
84 changed files with 1672 additions and 1346 deletions

View file

@ -1,51 +1,51 @@
name: Conformance Tests
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
push:
branches: [main]
pull_request:
workflow_dispatch:
concurrency:
group: conformance-${{ github.ref }}
cancel-in-progress: true
group: conformance-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
contents: read
jobs:
client-conformance:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- run: pnpm install
- run: pnpm run build:all
- run: pnpm run test:conformance:client:all
client-conformance:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- run: pnpm install
- run: pnpm run build:all
- run: pnpm run test:conformance:client:all
server-conformance:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- run: pnpm install
- run: pnpm run build:all
- run: pnpm run test:conformance:server
server-conformance:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- run: pnpm install
- run: pnpm run build:all
- run: pnpm run test:conformance:server

View file

@ -63,7 +63,8 @@ This repository has two main branches:
Small PRs get reviewed fast. Large PRs sit in the queue.
We can review a few dozen lines in a few minutes. But a PR touching hundreds of lines across many files takes real effort to verify—and things inevitably slip through. If your change is big, break it into a stack of smaller PRs or get clear alignment from a maintainer on your approach in an issue before submitting a large PR.
We can review a few dozen lines in a few minutes. But a PR touching hundreds of lines across many files takes real effort to verify—and things inevitably slip through. If your change is big, break it into a stack of smaller PRs or get clear alignment from a maintainer on your
approach in an issue before submitting a large PR.
### What Gets Rejected
@ -183,6 +184,4 @@ Please review our [Security Policy](SECURITY.md) for reporting security vulnerab
### License
By contributing, you agree that your code contributions will be licensed under
the Apache License 2.0. Documentation contributions (excluding specifications)
are licensed under CC-BY 4.0. See the [LICENSE](LICENSE) file for details.
By contributing, you agree that your code contributions will be licensed under the Apache License 2.0. Documentation contributions (excluding specifications) are licensed under CC-BY 4.0. See the [LICENSE](LICENSE) file for details.

View file

@ -1,7 +1,6 @@
# MCP TypeScript SDK
> [!IMPORTANT]
> **This is the `main` branch which contains v2 of the SDK (currently in development, pre-alpha).**
> [!IMPORTANT] **This is the `main` branch which contains v2 of the SDK (currently in development, pre-alpha).**
>
> We anticipate a stable v2 release in Q1 2026. Until then, **v1.x remains the recommended version** for production use. v1.x will continue to receive bug fixes and security updates for at least 6 months after v2 ships to give people time to upgrade.
>

View file

@ -8,6 +8,7 @@ import eslintConfigPrettier from 'eslint-config-prettier/flat';
import importPlugin from 'eslint-plugin-import';
import nodePlugin from 'eslint-plugin-n';
import simpleImportSortPlugin from 'eslint-plugin-simple-import-sort';
import eslintPluginUnicorn from 'eslint-plugin-unicorn';
import { configs } from 'typescript-eslint';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@ -17,6 +18,7 @@ export default defineConfig(
...configs.recommended,
importPlugin.flatConfigs.recommended,
importPlugin.flatConfigs.typescript,
eslintPluginUnicorn.configs.recommended,
{
languageOptions: {
parserOptions: {
@ -42,6 +44,10 @@ export default defineConfig(
}
},
rules: {
'unicorn/prevent-abbreviations': 'off',
'unicorn/no-null': 'off',
'unicorn/prefer-add-event-listener': 'off',
'unicorn/no-useless-undefined': ['error', { checkArguments: false }],
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'n/prefer-node-protocol': 'error',
'@typescript-eslint/consistent-type-imports': ['error', { disallowTypeAnnotations: false }],
@ -64,6 +70,12 @@ export default defineConfig(
optionalDependencies: false,
peerDependencies: true
}
],
'unicorn/filename-case': [
'error',
{
case: 'camelCase'
}
]
}
},

View file

@ -29,6 +29,7 @@
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-n": "catalog:devTools",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unicorn": "^62.0.0",
"prettier": "catalog:devTools",
"typescript": "catalog:devTools",
"typescript-eslint": "catalog:devTools"

View file

@ -14,7 +14,7 @@ export default defineConfig({
exclude: ignorePatterns,
deps: {
moduleDirectories: ['node_modules', path.resolve(__dirname, '../../packages'), path.resolve(__dirname, '../../common')]
},
}
},
poolOptions: {
threads: {

View file

@ -37,7 +37,6 @@ import { InMemoryOAuthClientProvider } from './simpleOAuthClientProvider.js';
// Set up OAuth (required for this example)
const OAUTH_CALLBACK_PORT = 8090; // Use different port than auth server (3001)
const OAUTH_CALLBACK_URL = `http://localhost:${OAUTH_CALLBACK_PORT}/callback`;
let oauthProvider: InMemoryOAuthClientProvider | undefined = undefined;
console.log('Getting OAuth token...');
const clientMetadata: OAuthClientMetadata = {
@ -48,7 +47,7 @@ const clientMetadata: OAuthClientMetadata = {
token_endpoint_auth_method: 'client_secret_post',
scope: 'mcp:tools'
};
oauthProvider = new InMemoryOAuthClientProvider(OAUTH_CALLBACK_URL, clientMetadata, (redirectUrl: URL) => {
const oauthProvider = new InMemoryOAuthClientProvider(OAUTH_CALLBACK_URL, clientMetadata, (redirectUrl: URL) => {
console.log(`🌐 Opening browser for OAuth redirect: ${redirectUrl.toString()}`);
openBrowser(redirectUrl.toString());
});
@ -64,7 +63,7 @@ let abortCommand = new AbortController();
let client: Client | null = null;
let transport: StreamableHTTPClientTransport | null = null;
let serverUrl = 'http://localhost:3000/mcp';
let sessionId: string | undefined = undefined;
let sessionId: string | undefined;
// Elicitation queue management
interface QueuedElicitation {
@ -99,6 +98,7 @@ async function main(): Promise<void> {
// Start the elicitation loop in the background
elicitationLoop().catch(error => {
console.error('Unexpected error in elicitation loop:', error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
});
@ -136,10 +136,10 @@ function printHelp(): void {
async function commandLoop(): Promise<void> {
await new Promise<void>(resolve => {
if (!isProcessingElicitations) {
resolve();
} else {
if (isProcessingElicitations) {
elicitationsCompleteSignal = resolve;
} else {
resolve();
}
});
@ -151,27 +151,32 @@ async function commandLoop(): Promise<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 {
@ -187,29 +192,35 @@ async function commandLoop(): Promise<void> {
await callTool(toolName, toolArgs);
}
break;
}
case 'payment-confirm':
case 'payment-confirm': {
await callPaymentConfirmTool();
break;
}
case 'third-party-auth':
case 'third-party-auth': {
await callThirdPartyAuthTool();
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}`);
@ -358,12 +369,12 @@ async function handleURLElicitation(params: ElicitRequestURLParams): Promise<Eli
}
// Example security warning to help prevent phishing attacks
console.log('\n⚠️ \x1b[33mSECURITY WARNING\x1b[0m ⚠️');
console.log('\x1b[33mThe server is requesting you to open an external URL.\x1b[0m');
console.log('\x1b[33mOnly proceed if you trust this server and understand why it needs this.\x1b[0m\n');
console.log(`🌐 Target domain: \x1b[36m${domain}\x1b[0m`);
console.log(`🔗 Full URL: \x1b[36m${url}\x1b[0m`);
console.log(`\nℹ️ Server's reason:\n\n\x1b[36m${message}\x1b[0m\n`);
console.log('\n⚠️ \u001B[33mSECURITY WARNING\u001B[0m ⚠️');
console.log('\u001B[33mThe server is requesting you to open an external URL.\u001B[0m');
console.log('\u001B[33mOnly proceed if you trust this server and understand why it needs this.\u001B[0m\n');
console.log(`🌐 Target domain: \u001B[36m${domain}\u001B[0m`);
console.log(`🔗 Full URL: \u001B[36m${url}\u001B[0m`);
console.log(`\nℹ️ Server's reason:\n\n\u001B[36m${message}\u001B[0m\n`);
// 1. Ask for user consent to open the URL
const consent = await new Promise<string>(resolve => {
@ -386,7 +397,7 @@ async function handleURLElicitation(params: ElicitRequestURLParams): Promise<Eli
const timeout = setTimeout(
() => {
pendingURLElicitations.delete(elicitationId);
console.log(`\x1b[31m❌ Elicitation ${elicitationId} timed out waiting for completion.\x1b[0m`);
console.log(`\u001B[31m❌ Elicitation ${elicitationId} timed out waiting for completion.\u001B[0m`);
reject(new Error('Elicitation completion timeout'));
},
5 * 60 * 1000
@ -440,7 +451,7 @@ async function waitForOAuthCallback(): Promise<string> {
const error = parsedUrl.searchParams.get('error');
if (code) {
console.log(`✅ Authorization code received: ${code?.substring(0, 10)}...`);
console.log(`✅ Authorization code received: ${code?.slice(0, 10)}...`);
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`
<html>
@ -454,7 +465,7 @@ async function waitForOAuthCallback(): Promise<string> {
`);
resolve(code);
setTimeout(() => server.close(), 15000);
setTimeout(() => server.close(), 15_000);
} else if (error) {
console.log(`❌ Authorization error: ${error}`);
res.writeHead(400, { 'Content-Type': 'text/html' });
@ -558,7 +569,7 @@ async function connect(url?: string): Promise<void> {
if (pending) {
clearTimeout(pending.timeout);
pendingURLElicitations.delete(elicitationId);
console.log(`\x1b[32m✅ Elicitation ${elicitationId} completed!\x1b[0m`);
console.log(`\u001B[32m✅ Elicitation ${elicitationId} completed!\u001B[0m`);
pending.resolve();
} else {
// Shouldn't happen - discard it!
@ -573,7 +584,7 @@ async function connect(url?: string): Promise<void> {
// Set up error handler after connection is established so we don't double log errors
client.onerror = error => {
console.error('\x1b[31mClient error:', error, '\x1b[0m');
console.error('\u001B[31mClient error:', error, '\u001B[0m');
};
} catch (error) {
console.error('Failed to connect:', error);
@ -611,7 +622,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;
@ -620,9 +634,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);
@ -683,30 +694,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) {
@ -749,6 +777,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);
}
@ -789,7 +818,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);
});
}

View file

@ -129,30 +129,34 @@ async function main(): Promise<void> {
// Display results from all clients
console.log('\n=== Final Results ===');
results.forEach(({ id, result }) => {
for (const { id, result } of results) {
console.log(`\n[${id}] Tool result:`);
if (Array.isArray(result.content)) {
result.content.forEach((item: { type: string; text?: string }) => {
for (const item of result.content) {
if (item.type === 'text' && item.text) {
console.log(` ${item.text}`);
} else {
console.log(` ${item.type} content:`, item);
}
});
}
} else {
console.log(` Unexpected result format:`, result);
}
});
}
console.log('\n=== All clients completed successfully ===');
} catch (error) {
console.error('Error running multiple clients:', error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}
}
// Start the example
main().catch((error: unknown) => {
console.error('Error running MCP multiple clients example:', error);
try {
await main();
} catch (error) {
console.error('Error running multiple clients:', error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
});
}

View file

@ -50,7 +50,7 @@ async function main(): Promise<void> {
console.log('List tools');
const toolsRequest = await listTools(client);
console.log('Tools: ', toolsRequest);
console.log('Tools:', toolsRequest);
// 2. Start multiple notification tools in parallel
console.log('\n=== Starting Multiple Notification Streams in Parallel ===');
@ -59,18 +59,18 @@ async function main(): Promise<void> {
// Log the results from each tool call
for (const [caller, result] of Object.entries(toolResults)) {
console.log(`\n=== Tool result for ${caller} ===`);
result.content.forEach((item: { type: string; text?: string }) => {
for (const item of result.content) {
if (item.type === 'text') {
console.log(` ${item.text}`);
} else {
console.log(` ${item.type} content:`, item);
}
});
}
}
// 3. Wait for all notifications (10 seconds)
console.log('\n=== Waiting for all notifications ===');
await new Promise(resolve => setTimeout(resolve, 10000));
await new Promise(resolve => setTimeout(resolve, 10_000));
// 4. Disconnect
console.log('\n=== Disconnecting ===');
@ -78,6 +78,7 @@ async function main(): Promise<void> {
console.log('Disconnected from MCP server');
} catch (error) {
console.error('Error running client:', error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}
}
@ -177,9 +178,9 @@ async function startParallelNotificationTools(client: Client): Promise<Record<st
// Organize results by caller
const resultsByTool: Record<string, CallToolResult> = {};
results.forEach(({ caller, result }) => {
for (const { caller, result } of results) {
resultsByTool[caller] = result;
});
}
return resultsByTool;
} catch (error) {
@ -188,8 +189,11 @@ async function startParallelNotificationTools(client: Client): Promise<Record<st
}
}
// Start the client
main().catch((error: unknown) => {
console.error('Error running MCP client:', error);
try {
// Run the client
await main();
} catch (error) {
console.error('Error running client:', error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
});
}

View file

@ -74,7 +74,10 @@ async function main() {
await transport.close();
}
main().catch(err => {
console.error(err);
try {
await main();
} catch (error) {
console.error('Error running client:', error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
});
}

View file

@ -84,7 +84,7 @@ class InteractiveOAuthClient {
const error = parsedUrl.searchParams.get('error');
if (code) {
console.log(`✅ Authorization code received: ${code?.substring(0, 10)}...`);
console.log(`✅ Authorization code received: ${code?.slice(0, 10)}...`);
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`
<html>
@ -256,13 +256,13 @@ class InteractiveOAuthClient {
if (result.tools && result.tools.length > 0) {
console.log('\n📋 Available tools:');
result.tools.forEach((tool, index) => {
for (const [index, tool] of result.tools.entries()) {
console.log(`${index + 1}. ${tool.name}`);
if (tool.description) {
console.log(` Description: ${tool.description}`);
}
console.log();
});
}
} else {
console.log('No tools available');
}
@ -314,13 +314,13 @@ class InteractiveOAuthClient {
console.log(`\n🔧 Tool '${toolName}' result:`);
if (result.content) {
result.content.forEach(content => {
for (const content of result.content) {
if (content.type === 'text') {
console.log(content.text);
} else {
console.log(content);
}
});
}
} else {
console.log(result);
}
@ -372,7 +372,7 @@ class InteractiveOAuthClient {
{
task: {
taskId: `task-${Date.now()}`,
ttl: 60000
ttl: 60_000
}
}
);
@ -380,32 +380,36 @@ class InteractiveOAuthClient {
// Iterate through all messages yielded by the generator
for await (const message of stream) {
switch (message.type) {
case 'taskCreated':
case 'taskCreated': {
console.log(`✓ Task created: ${message.task.taskId}`);
break;
}
case 'taskStatus':
case 'taskStatus': {
console.log(`⟳ Status: ${message.task.status}`);
if (message.task.statusMessage) {
console.log(` ${message.task.statusMessage}`);
}
break;
}
case 'result':
case 'result': {
console.log('✓ Completed!');
message.result.content.forEach(content => {
for (const content of message.result.content) {
if (content.type === 'text') {
console.log(content.text);
} else {
console.log(content);
}
});
}
break;
}
case 'error':
case 'error': {
console.log('✗ Error:');
console.log(` ${message.error.message}`);
break;
}
}
}
} catch (error) {
@ -456,8 +460,11 @@ async function main(): Promise<void> {
}
}
// Run if this file is executed directly
main().catch(error => {
console.error('Unhandled error:', error);
try {
// Run if this file is executed directly
await main();
} catch (error) {
console.error('Error running client:', error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
});
}

View file

@ -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);
});
}

View file

@ -130,23 +130,27 @@ async function run(url: string): Promise<void> {
const confirmStream = client.experimental.tasks.callToolStream(
{ name: 'confirm_delete', arguments: { filename: 'important.txt' } },
CallToolResultSchema,
{ task: { ttl: 60000 } }
{ task: { ttl: 60_000 } }
);
for await (const message of confirmStream) {
switch (message.type) {
case 'taskCreated':
case 'taskCreated': {
console.log(`Task created: ${message.task.taskId}`);
break;
case 'taskStatus':
}
case 'taskStatus': {
console.log(`Task status: ${message.task.status}`);
break;
case 'result':
}
case 'result': {
console.log(`Result: ${getTextContent(message.result)}`);
break;
case 'error':
}
case 'error': {
console.error(`Error: ${message.error}`);
break;
}
}
}
@ -158,24 +162,28 @@ async function run(url: string): Promise<void> {
{ name: 'write_haiku', arguments: { topic: 'autumn leaves' } },
CallToolResultSchema,
{
task: { ttl: 60000 }
task: { ttl: 60_000 }
}
);
for await (const message of haikuStream) {
switch (message.type) {
case 'taskCreated':
case 'taskCreated': {
console.log(`Task created: ${message.task.taskId}`);
break;
case 'taskStatus':
}
case 'taskStatus': {
console.log(`Task status: ${message.task.status}`);
break;
case 'result':
}
case 'result': {
console.log(`Result:\n${getTextContent(message.result)}`);
break;
case 'error':
}
case 'error': {
console.error(`Error: ${message.error}`);
break;
}
}
}
@ -197,7 +205,10 @@ for (let i = 0; i < args.length; i++) {
}
// Run the client
run(url).catch(error => {
try {
await run(url);
} catch (error) {
console.error('Error running client:', error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
});
}

View file

@ -106,4 +106,10 @@ async function main(): Promise<void> {
}
}
main().catch(console.error);
try {
await main();
} catch (error) {
console.error('Error running MCP client:', error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}

View file

@ -62,6 +62,7 @@ async function main(): Promise<void> {
console.log('Disconnected from MCP server');
} catch (error) {
console.error('Error running client:', error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}
}
@ -171,20 +172,23 @@ async function startNotificationTool(client: Client): Promise<void> {
const result = await client.request(request, CallToolResultSchema);
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 calling notification tool: ${error}`);
}
}
// Start the 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);
});
}

View file

@ -318,7 +318,7 @@ mcpServer.registerTool(
);
async function main() {
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
const PORT = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 3000;
const app = createMcpExpressApp();
@ -367,7 +367,7 @@ async function main() {
res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32000,
code: -32_000,
message: 'Bad Request: No valid session ID provided'
},
id: null
@ -383,7 +383,7 @@ async function main() {
res.status(500).json({
jsonrpc: '2.0',
error: {
code: -32603,
code: -32_603,
message: 'Internal server error'
},
id: null
@ -436,6 +436,7 @@ async function main() {
app.listen(PORT, error => {
if (error) {
console.error('Failed to start server:', error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}
console.log(`Form elicitation example server is running on http://localhost:${PORT}/mcp`);
@ -465,7 +466,10 @@ async function main() {
});
}
main().catch(error => {
try {
await main();
} catch (error) {
console.error('Server error:', error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
});
}

View file

@ -215,8 +215,8 @@ function completeURLElicitation(elicitationId: string) {
elicitation.completeResolver();
}
const MCP_PORT = process.env.MCP_PORT ? parseInt(process.env.MCP_PORT, 10) : 3000;
const AUTH_PORT = process.env.MCP_AUTH_PORT ? parseInt(process.env.MCP_AUTH_PORT, 10) : 3001;
const MCP_PORT = process.env.MCP_PORT ? Number.parseInt(process.env.MCP_PORT, 10) : 3000;
const AUTH_PORT = process.env.MCP_AUTH_PORT ? Number.parseInt(process.env.MCP_AUTH_PORT, 10) : 3001;
const app = createMcpExpressApp();
@ -277,15 +277,17 @@ async function sendApiKeyElicitation(
});
switch (result.action) {
case 'accept':
case 'accept': {
console.log('🔑 URL elicitation demo: Client accepted the API key elicitation (now pending form submission)');
// Wait for the API key to be submitted via the form
// The form submission will complete the elicitation
break;
default:
}
default: {
console.log('🔑 URL elicitation demo: Client declined to provide an API key');
// In a real app, this might close the connection, but for the demo, we'll continue
break;
}
}
} catch (error) {
console.error('Error during API key elicitation:', error);
@ -359,7 +361,7 @@ app.post('/api-key-form', express.urlencoded(), (req: Request, res: Response) =>
}
// A real app might store this API key to be used later for the user.
console.log(`🔑 Received API key \x1b[32m${apiKey}\x1b[0m for session ${sessionId}`);
console.log(`🔑 Received API key \u001B[32m${apiKey}\u001B[0m for session ${sessionId}`);
// If we have an elicitationId, complete the elicitation
completeURLElicitation(elicitationId);
@ -608,7 +610,7 @@ const mcpPostHandler = async (req: Request, res: Response) => {
res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32000,
code: -32_000,
message: 'Bad Request: No valid session ID provided'
},
id: null
@ -625,7 +627,7 @@ const mcpPostHandler = async (req: Request, res: Response) => {
res.status(500).json({
jsonrpc: '2.0',
error: {
code: -32603,
code: -32_603,
message: 'Internal server error'
},
id: null
@ -703,6 +705,7 @@ app.delete('/mcp', authMiddleware, mcpDeleteHandler);
app.listen(MCP_PORT, error => {
if (error) {
console.error('Failed to start server:', error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}
console.log(`MCP Streamable HTTP Server listening on port ${MCP_PORT}`);

View file

@ -59,15 +59,15 @@ app.get('/health', c => c.json({ status: 'ok' }));
app.all('/mcp', c => transport.handleRequest(c.req.raw));
// Start the server
const PORT = process.env.MCP_PORT ? parseInt(process.env.MCP_PORT, 10) : 3000;
const PORT = process.env.MCP_PORT ? Number.parseInt(process.env.MCP_PORT, 10) : 3000;
server.connect(transport).then(() => {
console.log(`Starting Hono MCP server on port ${PORT}`);
console.log(`Health check: http://localhost:${PORT}/health`);
console.log(`MCP endpoint: http://localhost:${PORT}/mcp`);
await server.connect(transport);
serve({
fetch: app.fetch,
port: PORT
});
console.log(`Starting Hono MCP server on port ${PORT}`);
console.log(`Health check: http://localhost:${PORT}/health`);
console.log(`MCP endpoint: http://localhost:${PORT}/mcp`);
serve({
fetch: app.fetch,
port: PORT
});

View file

@ -12,7 +12,7 @@ export class InMemoryEventStore implements EventStore {
* Generates a unique event ID for a given stream ID
*/
private generateEventId(streamId: string): string {
return `${streamId}_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
return `${streamId}_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
}
/**
@ -54,7 +54,7 @@ export class InMemoryEventStore implements EventStore {
let foundLastEvent = false;
// Sort events by eventId for chronological ordering
const sortedEvents = [...this.events.entries()].sort((a, b) => a[0].localeCompare(b[0]));
const sortedEvents = [...this.events.entries()].toSorted((a, b) => a[0].localeCompare(b[0]));
for (const [eventId, { streamId: eventStreamId, message }] of sortedEvents) {
// Only include events from the same stream

View file

@ -133,7 +133,7 @@ app.post('/mcp', async (req: Request, res: Response) => {
res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32000,
code: -32_000,
message: 'Bad Request: No valid session ID provided'
},
id: null
@ -149,7 +149,7 @@ app.post('/mcp', async (req: Request, res: Response) => {
res.status(500).json({
jsonrpc: '2.0',
error: {
code: -32603,
code: -32_603,
message: 'Internal server error'
},
id: null
@ -170,6 +170,7 @@ const PORT = 3000;
app.listen(PORT, error => {
if (error) {
console.error('Failed to start server:', error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}
console.log(`MCP Streamable HTTP Server listening on port ${PORT}`);

Some files were not shown because too many files have changed in this diff Show more