mcp-http-webhook 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +16 -0
- package/.prettierrc.json +8 -0
- package/ARCHITECTURE.md +269 -0
- package/CONTRIBUTING.md +136 -0
- package/GETTING_STARTED.md +310 -0
- package/IMPLEMENTATION.md +294 -0
- package/LICENSE +21 -0
- package/MIGRATION_TO_SDK.md +263 -0
- package/README.md +496 -0
- package/SDK_INTEGRATION_COMPLETE.md +300 -0
- package/STANDARD_SUBSCRIPTIONS.md +268 -0
- package/STANDARD_SUBSCRIPTIONS_COMPLETE.md +309 -0
- package/SUMMARY.md +272 -0
- package/Spec.md +2778 -0
- package/dist/errors/index.d.ts +52 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +81 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol/ProtocolHandler.d.ts +37 -0
- package/dist/protocol/ProtocolHandler.d.ts.map +1 -0
- package/dist/protocol/ProtocolHandler.js +172 -0
- package/dist/protocol/ProtocolHandler.js.map +1 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +502 -0
- package/dist/server.js.map +1 -0
- package/dist/stores/InMemoryStore.d.ts +27 -0
- package/dist/stores/InMemoryStore.d.ts.map +1 -0
- package/dist/stores/InMemoryStore.js +73 -0
- package/dist/stores/InMemoryStore.js.map +1 -0
- package/dist/stores/RedisStore.d.ts +18 -0
- package/dist/stores/RedisStore.d.ts.map +1 -0
- package/dist/stores/RedisStore.js +45 -0
- package/dist/stores/RedisStore.js.map +1 -0
- package/dist/stores/index.d.ts +3 -0
- package/dist/stores/index.d.ts.map +1 -0
- package/dist/stores/index.js +9 -0
- package/dist/stores/index.js.map +1 -0
- package/dist/subscriptions/SubscriptionManager.d.ts +49 -0
- package/dist/subscriptions/SubscriptionManager.d.ts.map +1 -0
- package/dist/subscriptions/SubscriptionManager.js +181 -0
- package/dist/subscriptions/SubscriptionManager.js.map +1 -0
- package/dist/types/index.d.ts +271 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +16 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/index.d.ts +51 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +154 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/webhooks/WebhookManager.d.ts +27 -0
- package/dist/webhooks/WebhookManager.d.ts.map +1 -0
- package/dist/webhooks/WebhookManager.js +174 -0
- package/dist/webhooks/WebhookManager.js.map +1 -0
- package/examples/GITHUB_LIVE_EXAMPLE.md +308 -0
- package/examples/GITHUB_LIVE_SETUP.md +253 -0
- package/examples/QUICKSTART.md +130 -0
- package/examples/basic-setup.ts +142 -0
- package/examples/github-server-live.ts +690 -0
- package/examples/github-server.ts +223 -0
- package/examples/google-drive-server-live.ts +773 -0
- package/examples/start-github-live.sh +53 -0
- package/jest.config.js +20 -0
- package/package.json +58 -0
- package/src/errors/index.ts +81 -0
- package/src/index.ts +19 -0
- package/src/server.ts +595 -0
- package/src/stores/InMemoryStore.ts +87 -0
- package/src/stores/RedisStore.ts +51 -0
- package/src/stores/index.ts +2 -0
- package/src/subscriptions/SubscriptionManager.ts +240 -0
- package/src/types/index.ts +341 -0
- package/src/utils/index.ts +156 -0
- package/src/webhooks/WebhookManager.ts +230 -0
- package/test-sdk-integration.sh +157 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import { nanoid } from 'nanoid';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generate a unique subscription ID
|
|
6
|
+
*/
|
|
7
|
+
export function generateSubscriptionId(): string {
|
|
8
|
+
return `sub_${nanoid(16)}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate webhook URL for third-party services
|
|
13
|
+
*/
|
|
14
|
+
export function generateWebhookUrl(publicUrl: string, subscriptionId: string): string {
|
|
15
|
+
const baseUrl = publicUrl.endsWith('/') ? publicUrl.slice(0, -1) : publicUrl;
|
|
16
|
+
return `${baseUrl}/webhooks/incoming/${subscriptionId}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parse URI template
|
|
21
|
+
* Example: parseUriTemplate('github://repo/{owner}/{repo}', 'github://repo/octocat/hello')
|
|
22
|
+
* Returns: { owner: 'octocat', repo: 'hello' }
|
|
23
|
+
*/
|
|
24
|
+
export function parseUriTemplate(template: string, uri: string): Record<string, string> {
|
|
25
|
+
const templateParts = template.split('/');
|
|
26
|
+
const uriParts = uri.split('/');
|
|
27
|
+
|
|
28
|
+
if (templateParts.length !== uriParts.length) {
|
|
29
|
+
throw new Error(`URI ${uri} does not match template ${template}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const params: Record<string, string> = {};
|
|
33
|
+
|
|
34
|
+
for (let i = 0; i < templateParts.length; i++) {
|
|
35
|
+
const templatePart = templateParts[i];
|
|
36
|
+
const uriPart = uriParts[i];
|
|
37
|
+
|
|
38
|
+
if (templatePart.startsWith('{') && templatePart.endsWith('}')) {
|
|
39
|
+
const paramName = templatePart.slice(1, -1);
|
|
40
|
+
params[paramName] = uriPart;
|
|
41
|
+
} else if (templatePart !== uriPart) {
|
|
42
|
+
throw new Error(`URI ${uri} does not match template ${template}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return params;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Match URI against template
|
|
51
|
+
*/
|
|
52
|
+
export function matchUriTemplate(template: string, uri: string): boolean {
|
|
53
|
+
try {
|
|
54
|
+
parseUriTemplate(template, uri);
|
|
55
|
+
return true;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create HMAC signature
|
|
63
|
+
*/
|
|
64
|
+
export function createHmacSignature(
|
|
65
|
+
payload: any,
|
|
66
|
+
secret: string,
|
|
67
|
+
algorithm: 'sha256' | 'sha1' = 'sha256'
|
|
68
|
+
): string {
|
|
69
|
+
const hmac = crypto.createHmac(algorithm, secret);
|
|
70
|
+
const data = typeof payload === 'string' ? payload : JSON.stringify(payload);
|
|
71
|
+
hmac.update(data);
|
|
72
|
+
return `${algorithm}=${hmac.digest('hex')}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Verify HMAC signature (timing-safe comparison)
|
|
77
|
+
*/
|
|
78
|
+
export function verifyHmacSignature(
|
|
79
|
+
payload: any,
|
|
80
|
+
signature: string,
|
|
81
|
+
secret: string,
|
|
82
|
+
algorithm: 'sha256' | 'sha1' = 'sha256'
|
|
83
|
+
): boolean {
|
|
84
|
+
try {
|
|
85
|
+
const expected = createHmacSignature(payload, secret, algorithm);
|
|
86
|
+
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Sleep utility for retry delays
|
|
94
|
+
*/
|
|
95
|
+
export function sleep(ms: number): Promise<void> {
|
|
96
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Exponential backoff calculator
|
|
101
|
+
*/
|
|
102
|
+
export function calculateBackoff(attempt: number, baseDelay: number): number {
|
|
103
|
+
return baseDelay * Math.pow(2, attempt);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Sanitize URL for logging (remove sensitive query params)
|
|
108
|
+
*/
|
|
109
|
+
export function sanitizeUrl(url: string): string {
|
|
110
|
+
try {
|
|
111
|
+
const urlObj = new URL(url);
|
|
112
|
+
urlObj.searchParams.delete('token');
|
|
113
|
+
urlObj.searchParams.delete('secret');
|
|
114
|
+
urlObj.searchParams.delete('api_key');
|
|
115
|
+
return urlObj.toString();
|
|
116
|
+
} catch {
|
|
117
|
+
return url;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Extract error message safely
|
|
123
|
+
*/
|
|
124
|
+
export function extractErrorMessage(error: unknown): string {
|
|
125
|
+
if (error instanceof Error) {
|
|
126
|
+
return error.message;
|
|
127
|
+
}
|
|
128
|
+
if (typeof error === 'string') {
|
|
129
|
+
return error;
|
|
130
|
+
}
|
|
131
|
+
return 'Unknown error';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Validate URL format
|
|
136
|
+
*/
|
|
137
|
+
export function isValidUrl(url: string): boolean {
|
|
138
|
+
try {
|
|
139
|
+
new URL(url);
|
|
140
|
+
return true;
|
|
141
|
+
} catch {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Validate HTTPS URL
|
|
148
|
+
*/
|
|
149
|
+
export function isHttpsUrl(url: string): boolean {
|
|
150
|
+
try {
|
|
151
|
+
const urlObj = new URL(url);
|
|
152
|
+
return urlObj.protocol === 'https:';
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import {
|
|
2
|
+
WebhookConfig,
|
|
3
|
+
StoredSubscription,
|
|
4
|
+
ResourceDefinition,
|
|
5
|
+
WebhookChangeInfo,
|
|
6
|
+
Logger,
|
|
7
|
+
} from '../types';
|
|
8
|
+
import { WebhookError } from '../errors';
|
|
9
|
+
import { sleep, calculateBackoff, extractErrorMessage } from '../utils';
|
|
10
|
+
|
|
11
|
+
export class WebhookManager {
|
|
12
|
+
private readonly timeout: number;
|
|
13
|
+
private readonly retries: number;
|
|
14
|
+
private readonly retryDelay: number;
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
private config: WebhookConfig | undefined,
|
|
18
|
+
private resources: ResourceDefinition[],
|
|
19
|
+
private logger?: Logger
|
|
20
|
+
) {
|
|
21
|
+
this.timeout = config?.outgoing?.timeout ?? 5000;
|
|
22
|
+
this.retries = config?.outgoing?.retries ?? 3;
|
|
23
|
+
this.retryDelay = config?.outgoing?.retryDelay ?? 1000;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Process incoming webhook from third-party service
|
|
28
|
+
*/
|
|
29
|
+
async processIncomingWebhook(
|
|
30
|
+
subscriptionId: string,
|
|
31
|
+
payload: any,
|
|
32
|
+
headers: Record<string, string>,
|
|
33
|
+
subscription: StoredSubscription
|
|
34
|
+
): Promise<WebhookChangeInfo | null> {
|
|
35
|
+
this.logger?.debug('Processing incoming webhook', {
|
|
36
|
+
subscriptionId,
|
|
37
|
+
resourceType: subscription.resourceType,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Find resource definition
|
|
41
|
+
const resource = this.findResourceByName(subscription.resourceType);
|
|
42
|
+
if (!resource?.subscription) {
|
|
43
|
+
throw new WebhookError('Resource subscription handler not found');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Verify signature if configured
|
|
47
|
+
if (this.config?.verifyIncomingSignature) {
|
|
48
|
+
const signature = headers['x-hub-signature-256'] || headers['x-signature'];
|
|
49
|
+
if (signature && this.config.incomingSecret) {
|
|
50
|
+
const isValid = this.config.verifyIncomingSignature(
|
|
51
|
+
payload,
|
|
52
|
+
signature,
|
|
53
|
+
this.config.incomingSecret
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
if (!isValid) {
|
|
57
|
+
this.logger?.warn('Invalid webhook signature', { subscriptionId });
|
|
58
|
+
throw new WebhookError('Invalid webhook signature');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
// Call resource's onWebhook handler
|
|
65
|
+
const changeInfo = await resource.subscription.onWebhook(subscriptionId, payload, headers);
|
|
66
|
+
|
|
67
|
+
this.logger?.debug('Webhook processed', {
|
|
68
|
+
subscriptionId,
|
|
69
|
+
changeType: changeInfo?.changeType,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return changeInfo;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
this.logger?.error('Failed to process webhook', {
|
|
75
|
+
subscriptionId,
|
|
76
|
+
error: extractErrorMessage(error),
|
|
77
|
+
});
|
|
78
|
+
throw new WebhookError('Failed to process webhook', { cause: error });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Send notification to client webhook in standard MCP format
|
|
84
|
+
*/
|
|
85
|
+
async notifyClient(
|
|
86
|
+
subscription: StoredSubscription,
|
|
87
|
+
changeInfo: WebhookChangeInfo
|
|
88
|
+
): Promise<void> {
|
|
89
|
+
// Standard MCP notification format
|
|
90
|
+
const payload = {
|
|
91
|
+
jsonrpc: '2.0',
|
|
92
|
+
method: 'notifications/resources/updated',
|
|
93
|
+
params: {
|
|
94
|
+
uri: changeInfo.resourceUri,
|
|
95
|
+
title: changeInfo.data?.title,
|
|
96
|
+
_meta: {
|
|
97
|
+
changeType: changeInfo.changeType,
|
|
98
|
+
subscriptionId: subscription.uri,
|
|
99
|
+
timestamp: new Date().toISOString(),
|
|
100
|
+
...changeInfo.data
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
this.logger?.info('Notifying client (MCP format)', {
|
|
106
|
+
url: subscription.clientCallbackUrl,
|
|
107
|
+
uri: changeInfo.resourceUri,
|
|
108
|
+
changeType: changeInfo.changeType,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Sign payload if secret is provided
|
|
112
|
+
let signature: string | undefined;
|
|
113
|
+
if (subscription.clientCallbackSecret && this.config?.outgoing?.signPayload) {
|
|
114
|
+
signature = this.config.outgoing.signPayload(payload, subscription.clientCallbackSecret);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Call before hook
|
|
118
|
+
this.config?.outgoing?.onBeforeCall?.(subscription.clientCallbackUrl, payload);
|
|
119
|
+
|
|
120
|
+
// Attempt delivery with retries
|
|
121
|
+
let lastError: Error | undefined;
|
|
122
|
+
|
|
123
|
+
for (let attempt = 0; attempt < this.retries; attempt++) {
|
|
124
|
+
try {
|
|
125
|
+
const response = await this.callWebhook(
|
|
126
|
+
subscription.clientCallbackUrl,
|
|
127
|
+
payload,
|
|
128
|
+
signature
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// Call after hook
|
|
132
|
+
this.config?.outgoing?.onAfterCall?.(subscription.clientCallbackUrl, response);
|
|
133
|
+
|
|
134
|
+
this.logger?.info('Client notified successfully (MCP format)', {
|
|
135
|
+
url: subscription.clientCallbackUrl,
|
|
136
|
+
attempt: attempt + 1,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return; // Success
|
|
140
|
+
} catch (error) {
|
|
141
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
142
|
+
|
|
143
|
+
this.logger?.warn('Webhook delivery failed', {
|
|
144
|
+
url: subscription.clientCallbackUrl,
|
|
145
|
+
attempt: attempt + 1,
|
|
146
|
+
error: extractErrorMessage(error),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Don't retry on client errors (4xx)
|
|
150
|
+
if (error instanceof Error && 'status' in error) {
|
|
151
|
+
const status = (error as any).status;
|
|
152
|
+
if (status >= 400 && status < 500) {
|
|
153
|
+
throw new WebhookError('Client error, not retrying', { status });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Wait before retry (exponential backoff)
|
|
158
|
+
if (attempt < this.retries - 1) {
|
|
159
|
+
const delay = calculateBackoff(attempt, this.retryDelay);
|
|
160
|
+
await sleep(delay);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// All retries failed
|
|
166
|
+
this.config?.outgoing?.onAfterCall?.(subscription.clientCallbackUrl, null, lastError);
|
|
167
|
+
|
|
168
|
+
throw new WebhookError('Failed to notify client after all retries', {
|
|
169
|
+
attempts: this.retries,
|
|
170
|
+
lastError: extractErrorMessage(lastError),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Call webhook URL
|
|
176
|
+
*/
|
|
177
|
+
private async callWebhook(
|
|
178
|
+
url: string,
|
|
179
|
+
payload: any,
|
|
180
|
+
signature?: string
|
|
181
|
+
): Promise<any> {
|
|
182
|
+
const controller = new AbortController();
|
|
183
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const headers: Record<string, string> = {
|
|
187
|
+
'Content-Type': 'application/json',
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
if (signature) {
|
|
191
|
+
headers['X-MCP-Signature'] = signature;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const response = await fetch(url, {
|
|
195
|
+
method: 'POST',
|
|
196
|
+
headers,
|
|
197
|
+
body: JSON.stringify(payload),
|
|
198
|
+
signal: controller.signal,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
clearTimeout(timeoutId);
|
|
202
|
+
|
|
203
|
+
if (!response.ok) {
|
|
204
|
+
const error: any = new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
205
|
+
error.status = response.status;
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
ok: true,
|
|
211
|
+
status: response.status,
|
|
212
|
+
};
|
|
213
|
+
} catch (error) {
|
|
214
|
+
clearTimeout(timeoutId);
|
|
215
|
+
|
|
216
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
217
|
+
throw new Error(`Request timeout after ${this.timeout}ms`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Find resource by name
|
|
226
|
+
*/
|
|
227
|
+
private findResourceByName(name: string): ResourceDefinition | undefined {
|
|
228
|
+
return this.resources.find((r) => r.name === name);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Test MCP SDK Integration
|
|
4
|
+
# This script verifies the server works with standard MCP clients
|
|
5
|
+
|
|
6
|
+
set -e
|
|
7
|
+
|
|
8
|
+
echo "๐งช Testing MCP SDK Integration..."
|
|
9
|
+
echo ""
|
|
10
|
+
|
|
11
|
+
# Build the project
|
|
12
|
+
echo "๐ฆ Building project..."
|
|
13
|
+
npm run build
|
|
14
|
+
echo "โ
Build successful"
|
|
15
|
+
echo ""
|
|
16
|
+
|
|
17
|
+
# Start the server in background
|
|
18
|
+
echo "๐ Starting server..."
|
|
19
|
+
node dist/examples/basic-setup.js &
|
|
20
|
+
SERVER_PID=$!
|
|
21
|
+
|
|
22
|
+
# Wait for server to start
|
|
23
|
+
sleep 3
|
|
24
|
+
|
|
25
|
+
# Function to cleanup
|
|
26
|
+
cleanup() {
|
|
27
|
+
echo ""
|
|
28
|
+
echo "๐งน Cleaning up..."
|
|
29
|
+
kill $SERVER_PID 2>/dev/null || true
|
|
30
|
+
}
|
|
31
|
+
trap cleanup EXIT
|
|
32
|
+
|
|
33
|
+
echo "โ
Server started (PID: $SERVER_PID)"
|
|
34
|
+
echo ""
|
|
35
|
+
|
|
36
|
+
# Test 1: Health check
|
|
37
|
+
echo "Test 1: Health check"
|
|
38
|
+
RESPONSE=$(curl -s http://localhost:3000/health)
|
|
39
|
+
if echo "$RESPONSE" | grep -q "ok"; then
|
|
40
|
+
echo "โ
Health check passed"
|
|
41
|
+
else
|
|
42
|
+
echo "โ Health check failed: $RESPONSE"
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
echo ""
|
|
46
|
+
|
|
47
|
+
# Test 2: Standard MCP endpoint - List tools
|
|
48
|
+
echo "Test 2: List tools via standard MCP endpoint"
|
|
49
|
+
RESPONSE=$(curl -s -X POST http://localhost:3000/mcp \
|
|
50
|
+
-H "Content-Type: application/json" \
|
|
51
|
+
-d '{
|
|
52
|
+
"jsonrpc": "2.0",
|
|
53
|
+
"id": 1,
|
|
54
|
+
"method": "tools/list"
|
|
55
|
+
}')
|
|
56
|
+
|
|
57
|
+
if echo "$RESPONSE" | grep -q "echo"; then
|
|
58
|
+
echo "โ
Tools list successful"
|
|
59
|
+
echo " Found tools: $(echo "$RESPONSE" | grep -o '"name":"[^"]*"' | cut -d'"' -f4 | tr '\n' ', ')"
|
|
60
|
+
else
|
|
61
|
+
echo "โ Tools list failed: $RESPONSE"
|
|
62
|
+
exit 1
|
|
63
|
+
fi
|
|
64
|
+
echo ""
|
|
65
|
+
|
|
66
|
+
# Test 3: Call a tool
|
|
67
|
+
echo "Test 3: Call echo tool"
|
|
68
|
+
RESPONSE=$(curl -s -X POST http://localhost:3000/mcp \
|
|
69
|
+
-H "Content-Type: application/json" \
|
|
70
|
+
-d '{
|
|
71
|
+
"jsonrpc": "2.0",
|
|
72
|
+
"id": 2,
|
|
73
|
+
"method": "tools/call",
|
|
74
|
+
"params": {
|
|
75
|
+
"name": "echo",
|
|
76
|
+
"arguments": {
|
|
77
|
+
"message": "Hello from MCP SDK!"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}')
|
|
81
|
+
|
|
82
|
+
if echo "$RESPONSE" | grep -q "Hello from MCP SDK"; then
|
|
83
|
+
echo "โ
Tool call successful"
|
|
84
|
+
else
|
|
85
|
+
echo "โ Tool call failed: $RESPONSE"
|
|
86
|
+
exit 1
|
|
87
|
+
fi
|
|
88
|
+
echo ""
|
|
89
|
+
|
|
90
|
+
# Test 4: List resources
|
|
91
|
+
echo "Test 4: List resources"
|
|
92
|
+
RESPONSE=$(curl -s -X POST http://localhost:3000/mcp \
|
|
93
|
+
-H "Content-Type: application/json" \
|
|
94
|
+
-d '{
|
|
95
|
+
"jsonrpc": "2.0",
|
|
96
|
+
"id": 3,
|
|
97
|
+
"method": "resources/list"
|
|
98
|
+
}')
|
|
99
|
+
|
|
100
|
+
if echo "$RESPONSE" | grep -q "Greeting Resource"; then
|
|
101
|
+
echo "โ
Resources list successful"
|
|
102
|
+
else
|
|
103
|
+
echo "โ Resources list failed: $RESPONSE"
|
|
104
|
+
exit 1
|
|
105
|
+
fi
|
|
106
|
+
echo ""
|
|
107
|
+
|
|
108
|
+
# Test 5: Read a resource
|
|
109
|
+
echo "Test 5: Read a resource"
|
|
110
|
+
RESPONSE=$(curl -s -X POST http://localhost:3000/mcp \
|
|
111
|
+
-H "Content-Type: application/json" \
|
|
112
|
+
-d '{
|
|
113
|
+
"jsonrpc": "2.0",
|
|
114
|
+
"id": 4,
|
|
115
|
+
"method": "resources/read",
|
|
116
|
+
"params": {
|
|
117
|
+
"uri": "example://greeting/world"
|
|
118
|
+
}
|
|
119
|
+
}')
|
|
120
|
+
|
|
121
|
+
if echo "$RESPONSE" | grep -q "Hello"; then
|
|
122
|
+
echo "โ
Resource read successful"
|
|
123
|
+
else
|
|
124
|
+
echo "โ Resource read failed: $RESPONSE"
|
|
125
|
+
exit 1
|
|
126
|
+
fi
|
|
127
|
+
echo ""
|
|
128
|
+
|
|
129
|
+
# Test 6: Webhook subscription endpoint (extension)
|
|
130
|
+
echo "Test 6: Webhook subscription endpoint (extension)"
|
|
131
|
+
RESPONSE=$(curl -s -X POST http://localhost:3000/mcp/resources/subscribe \
|
|
132
|
+
-H "Content-Type: application/json" \
|
|
133
|
+
-d '{
|
|
134
|
+
"uri": "example://greeting/alice",
|
|
135
|
+
"callbackUrl": "https://example.com/webhook",
|
|
136
|
+
"callbackSecret": "test-secret"
|
|
137
|
+
}')
|
|
138
|
+
|
|
139
|
+
if echo "$RESPONSE" | grep -q "subscriptionId"; then
|
|
140
|
+
echo "โ
Webhook subscription successful"
|
|
141
|
+
SUBSCRIPTION_ID=$(echo "$RESPONSE" | grep -o '"subscriptionId":"[^"]*"' | cut -d'"' -f4)
|
|
142
|
+
echo " Subscription ID: $SUBSCRIPTION_ID"
|
|
143
|
+
else
|
|
144
|
+
echo "โ Webhook subscription failed: $RESPONSE"
|
|
145
|
+
exit 1
|
|
146
|
+
fi
|
|
147
|
+
echo ""
|
|
148
|
+
|
|
149
|
+
echo "๐ All tests passed!"
|
|
150
|
+
echo ""
|
|
151
|
+
echo "โจ MCP SDK Integration is working correctly!"
|
|
152
|
+
echo ""
|
|
153
|
+
echo "You can now:"
|
|
154
|
+
echo " - Use with MCP Inspector: npx @modelcontextprotocol/inspector http://localhost:3000/mcp"
|
|
155
|
+
echo " - Configure Claude Desktop to use: http://localhost:3000/mcp"
|
|
156
|
+
echo " - Integrate with any standard MCP client"
|
|
157
|
+
echo ""
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"declarationMap": true,
|
|
10
|
+
"sourceMap": true,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"resolveJsonModule": true,
|
|
16
|
+
"moduleResolution": "node",
|
|
17
|
+
"types": ["node", "jest"]
|
|
18
|
+
},
|
|
19
|
+
"include": ["src/**/*"],
|
|
20
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
21
|
+
}
|