v2 ESlint fixes (#1403)
Co-authored-by: Matt <77928207+mattzcarey@users.noreply.github.com>
This commit is contained in:
parent
9296459ac0
commit
9fc7e28f48
84 changed files with 1672 additions and 1346 deletions
82
.github/workflows/conformance.yml
vendored
82
.github/workflows/conformance.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export default defineConfig({
|
|||
exclude: ignorePatterns,
|
||||
deps: {
|
||||
moduleDirectories: ['node_modules', path.resolve(__dirname, '../../packages'), path.resolve(__dirname, '../../common')]
|
||||
},
|
||||
}
|
||||
},
|
||||
poolOptions: {
|
||||
threads: {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue