cc-jandi 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/LICENSE +21 -0
- package/README.md +658 -0
- package/dist/index.js +7 -0
- package/dist/services/IncomingWebhookService.js +75 -0
- package/dist/services/TeamIncomingWebhookService.js +97 -0
- package/dist/services/base/BaseWebhookService.js +90 -0
- package/dist/services/configService.js +125 -0
- package/dist/services/index.js +4 -0
- package/dist/tools/GenerateWebhookScriptTool.js +232 -0
- package/dist/tools/incoming/SendMessageTool.js +48 -0
- package/dist/tools/incoming/SendRichMessageTool.js +74 -0
- package/dist/tools/incoming/TestWebhookTool.js +95 -0
- package/dist/tools/incoming/ValidateTokenTool.js +54 -0
- package/dist/tools/outgoing/GenerateOutgoingHandlerTool.js +217 -0
- package/dist/tools/outgoing/SimulateOutgoingPayloadTool.js +103 -0
- package/dist/tools/outgoing/ValidateOutgoingResponseTool.js +96 -0
- package/dist/tools/team-incoming/SendTeamMessageTool.js +57 -0
- package/dist/tools/team-incoming/SendTeamRichMessageTool.js +83 -0
- package/dist/tools/team-incoming/ValidateTeamTokenTool.js +56 -0
- package/dist/types/common.js +19 -0
- package/dist/types/incoming.js +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/jandi.js +3 -0
- package/dist/types/outgoing.js +1 -0
- package/dist/types/team-incoming.js +1 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/resolveTeamToken.js +23 -0
- package/dist/utils/resolveToken.js +30 -0
- package/dist/utils/validateColor.js +9 -0
- package/package.json +61 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { MCPTool } from "mcp-framework";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { IncomingWebhookService } from "../../services/IncomingWebhookService.js";
|
|
4
|
+
import { resolveIncomingToken } from "../../utils/resolveToken.js";
|
|
5
|
+
import { validateHexColor } from "../../utils/validateColor.js";
|
|
6
|
+
import { JandiColors } from "../../types/common.js";
|
|
7
|
+
class SendRichMessageTool extends MCPTool {
|
|
8
|
+
name = "send_rich_message";
|
|
9
|
+
description = "Send a rich message with color and attachments to Jandi channel via Incoming Webhook";
|
|
10
|
+
schema = {
|
|
11
|
+
message: {
|
|
12
|
+
type: z.string(),
|
|
13
|
+
description: "The main message text to send to Jandi",
|
|
14
|
+
},
|
|
15
|
+
color: {
|
|
16
|
+
type: z.string().optional(),
|
|
17
|
+
description: "Hex color code for the message attachment (e.g., '#FF0000' for red). Default is Jandi's default color",
|
|
18
|
+
},
|
|
19
|
+
connectInfo: {
|
|
20
|
+
type: z.array(z.object({
|
|
21
|
+
title: z.string().optional(),
|
|
22
|
+
description: z.string().optional(),
|
|
23
|
+
imageUrl: z.string().optional(),
|
|
24
|
+
})).optional(),
|
|
25
|
+
description: "Array of additional information sections with optional title, description, and image URL",
|
|
26
|
+
},
|
|
27
|
+
token: {
|
|
28
|
+
type: z.string().optional(),
|
|
29
|
+
description: "Jandi webhook token (32-character hexadecimal string). If not provided, will use tokenAlias or default token",
|
|
30
|
+
},
|
|
31
|
+
tokenAlias: {
|
|
32
|
+
type: z.string().optional(),
|
|
33
|
+
description: "Token alias from configuration (e.g., 'default', 'dev', 'prod'). If not provided, will use 'default'",
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
async execute(input) {
|
|
37
|
+
try {
|
|
38
|
+
const resolved = resolveIncomingToken(input);
|
|
39
|
+
if (!resolved.success) {
|
|
40
|
+
return { success: false, error: resolved.error };
|
|
41
|
+
}
|
|
42
|
+
let normalizedColor = input.color;
|
|
43
|
+
if (input.color) {
|
|
44
|
+
const colorResult = validateHexColor(input.color);
|
|
45
|
+
if (!colorResult.valid) {
|
|
46
|
+
return { success: false, error: colorResult.error };
|
|
47
|
+
}
|
|
48
|
+
normalizedColor = colorResult.normalized;
|
|
49
|
+
}
|
|
50
|
+
const message = IncomingWebhookService.createRichMessage(input.message, normalizedColor, input.connectInfo);
|
|
51
|
+
const result = await IncomingWebhookService.sendMessage(resolved.config, message);
|
|
52
|
+
if (result.success) {
|
|
53
|
+
return {
|
|
54
|
+
success: true,
|
|
55
|
+
data: {
|
|
56
|
+
message: "Rich message sent successfully to Jandi",
|
|
57
|
+
tokenUsed: resolved.config.alias || 'direct',
|
|
58
|
+
messageDetails: {
|
|
59
|
+
color: input.color || JandiColors.DEFAULT,
|
|
60
|
+
attachments: input.connectInfo?.length || 0
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
return { success: false, error: result.error };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
return { success: false, error: `Unexpected error: ${error}` };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export default SendRichMessageTool;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { MCPTool } from "mcp-framework";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { IncomingWebhookService } from "../../services/IncomingWebhookService.js";
|
|
4
|
+
import { resolveIncomingToken } from "../../utils/resolveToken.js";
|
|
5
|
+
import { JandiColors, MessageType } from "../../types/common.js";
|
|
6
|
+
class TestWebhookTool extends MCPTool {
|
|
7
|
+
name = "test_webhook";
|
|
8
|
+
description = "Test Jandi Incoming Webhook connection by sending various test messages";
|
|
9
|
+
schema = {
|
|
10
|
+
token: {
|
|
11
|
+
type: z.string().optional(),
|
|
12
|
+
description: "Jandi webhook token to test (32-character hexadecimal string). If not provided, will use tokenAlias or default token",
|
|
13
|
+
},
|
|
14
|
+
tokenAlias: {
|
|
15
|
+
type: z.string().optional(),
|
|
16
|
+
description: "Token alias from configuration to test (e.g., 'default', 'dev', 'prod'). If not provided, will use 'default'",
|
|
17
|
+
},
|
|
18
|
+
testType: {
|
|
19
|
+
type: z.enum(['basic', 'rich', 'all']).optional(),
|
|
20
|
+
description: "Type of test to perform: 'basic' (simple message), 'rich' (rich message), 'all' (comprehensive test). Default is 'basic'",
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
async execute(input) {
|
|
24
|
+
try {
|
|
25
|
+
const resolved = resolveIncomingToken(input);
|
|
26
|
+
if (!resolved.success) {
|
|
27
|
+
return { success: false, error: resolved.error };
|
|
28
|
+
}
|
|
29
|
+
const config = resolved.config;
|
|
30
|
+
const testType = input.testType || 'basic';
|
|
31
|
+
const timestamp = new Date().toISOString();
|
|
32
|
+
const results = [];
|
|
33
|
+
if (testType === 'basic' || testType === 'all') {
|
|
34
|
+
const basicMessage = IncomingWebhookService.createBasicMessage(`🧪 Basic webhook test - ${timestamp}`);
|
|
35
|
+
const basicResult = await IncomingWebhookService.sendMessage(config, basicMessage);
|
|
36
|
+
results.push({
|
|
37
|
+
type: 'basic',
|
|
38
|
+
success: basicResult.success,
|
|
39
|
+
error: basicResult.error
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (testType === 'rich' || testType === 'all') {
|
|
43
|
+
const richMessage = IncomingWebhookService.createRichMessage(`🎨 Rich webhook test - ${timestamp}`, JandiColors.BLUE, [{
|
|
44
|
+
title: 'Test Section',
|
|
45
|
+
description: 'This is a test of rich message functionality with color and attachments.',
|
|
46
|
+
}]);
|
|
47
|
+
const richResult = await IncomingWebhookService.sendMessage(config, richMessage);
|
|
48
|
+
results.push({
|
|
49
|
+
type: 'rich',
|
|
50
|
+
success: richResult.success,
|
|
51
|
+
error: richResult.error
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (testType === 'all') {
|
|
55
|
+
const statusTypes = [MessageType.SUCCESS, MessageType.WARNING, MessageType.ERROR];
|
|
56
|
+
for (const statusType of statusTypes) {
|
|
57
|
+
const statusMessage = IncomingWebhookService.createStatusMessage(`${statusType.toUpperCase()} status test - ${timestamp}`, statusType, `This is a test of ${statusType} message type`);
|
|
58
|
+
const statusResult = await IncomingWebhookService.sendMessage(config, statusMessage);
|
|
59
|
+
results.push({
|
|
60
|
+
type: `status_${statusType}`,
|
|
61
|
+
success: statusResult.success,
|
|
62
|
+
error: statusResult.error
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const successCount = results.filter(r => r.success).length;
|
|
67
|
+
const totalCount = results.length;
|
|
68
|
+
const allSuccessful = successCount === totalCount;
|
|
69
|
+
return {
|
|
70
|
+
success: allSuccessful,
|
|
71
|
+
data: {
|
|
72
|
+
message: allSuccessful
|
|
73
|
+
? `All ${totalCount} webhook tests passed successfully`
|
|
74
|
+
: `${successCount}/${totalCount} webhook tests passed`,
|
|
75
|
+
tokenUsed: config.alias || 'direct',
|
|
76
|
+
testType,
|
|
77
|
+
timestamp,
|
|
78
|
+
results,
|
|
79
|
+
summary: {
|
|
80
|
+
total: totalCount,
|
|
81
|
+
successful: successCount,
|
|
82
|
+
failed: totalCount - successCount
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
return {
|
|
89
|
+
success: false,
|
|
90
|
+
error: `Unexpected error during webhook test: ${error}`
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export default TestWebhookTool;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { MCPTool } from "mcp-framework";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { IncomingWebhookService } from "../../services/IncomingWebhookService.js";
|
|
4
|
+
import { resolveIncomingToken } from "../../utils/resolveToken.js";
|
|
5
|
+
class ValidateTokenTool extends MCPTool {
|
|
6
|
+
name = "validate_token";
|
|
7
|
+
description = "Validate a Jandi Incoming Webhook token by sending a test message";
|
|
8
|
+
schema = {
|
|
9
|
+
token: {
|
|
10
|
+
type: z.string().optional(),
|
|
11
|
+
description: "Jandi webhook token to validate (32-character hexadecimal string). If not provided, will use tokenAlias or default token",
|
|
12
|
+
},
|
|
13
|
+
tokenAlias: {
|
|
14
|
+
type: z.string().optional(),
|
|
15
|
+
description: "Token alias from configuration to validate (e.g., 'default', 'dev', 'prod'). If not provided, will use 'default'",
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
async execute(input) {
|
|
19
|
+
try {
|
|
20
|
+
const resolved = resolveIncomingToken(input);
|
|
21
|
+
if (!resolved.success) {
|
|
22
|
+
return { success: false, error: resolved.error };
|
|
23
|
+
}
|
|
24
|
+
const result = await IncomingWebhookService.validateToken(resolved.config.token, resolved.config.url);
|
|
25
|
+
if (result.success) {
|
|
26
|
+
return {
|
|
27
|
+
success: true,
|
|
28
|
+
data: {
|
|
29
|
+
message: "Token is valid and webhook is working",
|
|
30
|
+
tokenAlias: resolved.config.alias || 'direct',
|
|
31
|
+
tokenFormat: "valid"
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
return {
|
|
37
|
+
success: false,
|
|
38
|
+
error: result.error,
|
|
39
|
+
data: {
|
|
40
|
+
tokenAlias: resolved.config.alias || 'direct',
|
|
41
|
+
tokenFormat: "valid_format_but_failed"
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
return {
|
|
48
|
+
success: false,
|
|
49
|
+
error: `Unexpected error during token validation: ${error}`
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export default ValidateTokenTool;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { MCPTool } from "mcp-framework";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
class GenerateOutgoingHandlerTool extends MCPTool {
|
|
4
|
+
name = "generate_outgoing_handler";
|
|
5
|
+
description = "Generate a server handler for Jandi Outgoing Webhook. Creates ready-to-run code for receiving and responding to Jandi outgoing webhook events.";
|
|
6
|
+
schema = {
|
|
7
|
+
framework: {
|
|
8
|
+
type: z.enum(['express', 'fastapi', 'flask']),
|
|
9
|
+
description: "Server framework to generate the handler for: 'express' (Node.js), 'fastapi' (Python), 'flask' (Python)",
|
|
10
|
+
},
|
|
11
|
+
verificationToken: {
|
|
12
|
+
type: z.string().optional(),
|
|
13
|
+
description: "Verification token to validate incoming requests from Jandi. If provided, the handler will verify the token.",
|
|
14
|
+
},
|
|
15
|
+
handlerLogic: {
|
|
16
|
+
type: z.string().optional(),
|
|
17
|
+
description: "Custom handler logic description. Default generates an echo bot that responds with the received message.",
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
async execute(input) {
|
|
21
|
+
try {
|
|
22
|
+
let script;
|
|
23
|
+
let fileName;
|
|
24
|
+
let executionInstructions;
|
|
25
|
+
switch (input.framework) {
|
|
26
|
+
case 'express':
|
|
27
|
+
script = this.generateExpressHandler(input);
|
|
28
|
+
fileName = 'jandi-webhook-handler.js';
|
|
29
|
+
executionInstructions = 'npm init -y && npm install express && node jandi-webhook-handler.js';
|
|
30
|
+
break;
|
|
31
|
+
case 'fastapi':
|
|
32
|
+
script = this.generateFastAPIHandler(input);
|
|
33
|
+
fileName = 'jandi_webhook_handler.py';
|
|
34
|
+
executionInstructions = 'pip install fastapi uvicorn && uvicorn jandi_webhook_handler:app --reload --port 3000';
|
|
35
|
+
break;
|
|
36
|
+
case 'flask':
|
|
37
|
+
script = this.generateFlaskHandler(input);
|
|
38
|
+
fileName = 'jandi_webhook_handler.py';
|
|
39
|
+
executionInstructions = 'pip install flask && python jandi_webhook_handler.py';
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
success: true,
|
|
44
|
+
data: {
|
|
45
|
+
framework: input.framework,
|
|
46
|
+
fileName,
|
|
47
|
+
executionInstructions,
|
|
48
|
+
script,
|
|
49
|
+
message: `Successfully generated ${input.framework} handler for Jandi Outgoing Webhook`,
|
|
50
|
+
notes: [
|
|
51
|
+
"The handler listens on port 3000 by default",
|
|
52
|
+
"Use simulate_outgoing_payload to generate test payloads",
|
|
53
|
+
"Response format: { body, connectColor?, connectInfo? }",
|
|
54
|
+
"Response body max 5000 chars, total response max 256KB"
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
return { success: false, error: `Error generating handler: ${error}` };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
generateExpressHandler(input) {
|
|
64
|
+
const tokenCheck = input.verificationToken
|
|
65
|
+
? `
|
|
66
|
+
// Verify token
|
|
67
|
+
if (req.body.token !== '${input.verificationToken}') {
|
|
68
|
+
return res.status(401).json({ error: 'Invalid token' });
|
|
69
|
+
}
|
|
70
|
+
`
|
|
71
|
+
: '';
|
|
72
|
+
return `const express = require('express');
|
|
73
|
+
const app = express();
|
|
74
|
+
const PORT = 3000;
|
|
75
|
+
|
|
76
|
+
app.use(express.json());
|
|
77
|
+
|
|
78
|
+
// Jandi Outgoing Webhook Handler
|
|
79
|
+
app.post('/webhook', (req, res) => {
|
|
80
|
+
const { token, teamName, roomName, writerName, writerEmail, text, keyword, createdAt } = req.body;
|
|
81
|
+
// Team Outgoing Webhook uses writer object: req.body.writer.name, req.body.writer.email
|
|
82
|
+
${tokenCheck}
|
|
83
|
+
console.log(\`[\${new Date().toISOString()}] Message from \${writerName || req.body.writer?.name}: \${text}\`);
|
|
84
|
+
|
|
85
|
+
// Respond with a Jandi message format
|
|
86
|
+
// Return empty 200 to acknowledge without sending a response message
|
|
87
|
+
res.json({
|
|
88
|
+
body: \`Received: \${text}\`,
|
|
89
|
+
connectColor: "#FAC11B",
|
|
90
|
+
connectInfo: [{
|
|
91
|
+
title: "Webhook Response",
|
|
92
|
+
description: \`Processed message from \${writerName || req.body.writer?.name} in \${roomName}\`
|
|
93
|
+
}]
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
app.listen(PORT, () => {
|
|
98
|
+
console.log(\`Jandi Outgoing Webhook handler listening on port \${PORT}\`);
|
|
99
|
+
console.log(\`Endpoint: http://localhost:\${PORT}/webhook\`);
|
|
100
|
+
});
|
|
101
|
+
`;
|
|
102
|
+
}
|
|
103
|
+
generateFastAPIHandler(input) {
|
|
104
|
+
const tokenCheck = input.verificationToken
|
|
105
|
+
? `
|
|
106
|
+
# Verify token
|
|
107
|
+
if payload.token != "${input.verificationToken}":
|
|
108
|
+
raise HTTPException(status_code=401, detail="Invalid token")
|
|
109
|
+
`
|
|
110
|
+
: '';
|
|
111
|
+
return `from fastapi import FastAPI, HTTPException
|
|
112
|
+
from pydantic import BaseModel
|
|
113
|
+
from typing import Optional, Dict, Any, List
|
|
114
|
+
from datetime import datetime
|
|
115
|
+
import uvicorn
|
|
116
|
+
|
|
117
|
+
app = FastAPI(title="Jandi Outgoing Webhook Handler")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class Writer(BaseModel):
|
|
121
|
+
id: Optional[str] = None
|
|
122
|
+
name: str
|
|
123
|
+
email: str
|
|
124
|
+
phoneNumber: Optional[str] = None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class OutgoingPayload(BaseModel):
|
|
128
|
+
token: str
|
|
129
|
+
teamName: str
|
|
130
|
+
roomName: str
|
|
131
|
+
writerName: Optional[str] = None # Standard Outgoing
|
|
132
|
+
writerEmail: Optional[str] = None # Standard Outgoing
|
|
133
|
+
writer: Optional[Writer] = None # Team Outgoing
|
|
134
|
+
text: str
|
|
135
|
+
keyword: str
|
|
136
|
+
createdAt: str
|
|
137
|
+
data: Optional[Dict[str, Any]] = None
|
|
138
|
+
platform: Optional[str] = None
|
|
139
|
+
ip: Optional[str] = None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class ConnectInfo(BaseModel):
|
|
143
|
+
title: Optional[str] = None
|
|
144
|
+
description: Optional[str] = None
|
|
145
|
+
imageUrl: Optional[str] = None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class WebhookResponse(BaseModel):
|
|
149
|
+
body: str
|
|
150
|
+
connectColor: Optional[str] = "#FAC11B"
|
|
151
|
+
connectInfo: Optional[List[ConnectInfo]] = None
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@app.post("/webhook", response_model=WebhookResponse)
|
|
155
|
+
async def handle_webhook(payload: OutgoingPayload):
|
|
156
|
+
${tokenCheck}
|
|
157
|
+
writer_name = payload.writerName or (payload.writer.name if payload.writer else "Unknown")
|
|
158
|
+
print(f"[{datetime.now().isoformat()}] Message from {writer_name}: {payload.text}")
|
|
159
|
+
|
|
160
|
+
return WebhookResponse(
|
|
161
|
+
body=f"Received: {payload.text}",
|
|
162
|
+
connectColor="#FAC11B",
|
|
163
|
+
connectInfo=[ConnectInfo(
|
|
164
|
+
title="Webhook Response",
|
|
165
|
+
description=f"Processed message from {writer_name} in {payload.roomName}"
|
|
166
|
+
)]
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
if __name__ == "__main__":
|
|
171
|
+
uvicorn.run(app, host="0.0.0.0", port=3000)
|
|
172
|
+
`;
|
|
173
|
+
}
|
|
174
|
+
generateFlaskHandler(input) {
|
|
175
|
+
const tokenCheck = input.verificationToken
|
|
176
|
+
? `
|
|
177
|
+
# Verify token
|
|
178
|
+
if data.get('token') != '${input.verificationToken}':
|
|
179
|
+
return jsonify({'error': 'Invalid token'}), 401
|
|
180
|
+
`
|
|
181
|
+
: '';
|
|
182
|
+
return `from flask import Flask, request, jsonify
|
|
183
|
+
from datetime import datetime
|
|
184
|
+
|
|
185
|
+
app = Flask(__name__)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@app.route('/webhook', methods=['POST'])
|
|
189
|
+
def handle_webhook():
|
|
190
|
+
data = request.get_json()
|
|
191
|
+
${tokenCheck}
|
|
192
|
+
# Support both standard and team outgoing webhook formats
|
|
193
|
+
writer_name = data.get('writerName') or data.get('writer', {}).get('name', 'Unknown')
|
|
194
|
+
text = data.get('text', '')
|
|
195
|
+
room_name = data.get('roomName', '')
|
|
196
|
+
|
|
197
|
+
print(f"[{datetime.now().isoformat()}] Message from {writer_name}: {text}")
|
|
198
|
+
|
|
199
|
+
# Respond with Jandi message format
|
|
200
|
+
return jsonify({
|
|
201
|
+
'body': f'Received: {text}',
|
|
202
|
+
'connectColor': '#FAC11B',
|
|
203
|
+
'connectInfo': [{
|
|
204
|
+
'title': 'Webhook Response',
|
|
205
|
+
'description': f'Processed message from {writer_name} in {room_name}'
|
|
206
|
+
}]
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
if __name__ == '__main__':
|
|
211
|
+
print('Jandi Outgoing Webhook handler listening on port 3000')
|
|
212
|
+
print('Endpoint: http://localhost:3000/webhook')
|
|
213
|
+
app.run(host='0.0.0.0', port=3000, debug=True)
|
|
214
|
+
`;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
export default GenerateOutgoingHandlerTool;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { MCPTool } from "mcp-framework";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
class SimulateOutgoingPayloadTool extends MCPTool {
|
|
4
|
+
name = "simulate_outgoing_payload";
|
|
5
|
+
description = "Generate a simulated Jandi Outgoing Webhook payload JSON for testing your webhook handler server. Useful for local development and testing.";
|
|
6
|
+
schema = {
|
|
7
|
+
webhookType: {
|
|
8
|
+
type: z.enum(['outgoing', 'team-outgoing']).optional(),
|
|
9
|
+
description: "Type of outgoing webhook: 'outgoing' (standard) or 'team-outgoing' (team). Default is 'outgoing'",
|
|
10
|
+
},
|
|
11
|
+
text: {
|
|
12
|
+
type: z.string(),
|
|
13
|
+
description: "The message text that triggered the webhook",
|
|
14
|
+
},
|
|
15
|
+
keyword: {
|
|
16
|
+
type: z.string().optional(),
|
|
17
|
+
description: "The keyword that triggered the outgoing webhook. Default is 'test'",
|
|
18
|
+
},
|
|
19
|
+
teamName: {
|
|
20
|
+
type: z.string().optional(),
|
|
21
|
+
description: "Team name for the payload. Default is 'TestTeam'",
|
|
22
|
+
},
|
|
23
|
+
roomName: {
|
|
24
|
+
type: z.string().optional(),
|
|
25
|
+
description: "Chat room name for the payload. Default is 'TestRoom'",
|
|
26
|
+
},
|
|
27
|
+
writerName: {
|
|
28
|
+
type: z.string().optional(),
|
|
29
|
+
description: "Writer name for the payload. Default is 'TestUser'",
|
|
30
|
+
},
|
|
31
|
+
writerEmail: {
|
|
32
|
+
type: z.string().optional(),
|
|
33
|
+
description: "Writer email for the payload. Default is 'test@example.com'",
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
async execute(input) {
|
|
37
|
+
try {
|
|
38
|
+
const webhookType = input.webhookType || 'outgoing';
|
|
39
|
+
const now = new Date().toISOString();
|
|
40
|
+
if (webhookType === 'team-outgoing') {
|
|
41
|
+
const payload = {
|
|
42
|
+
token: "abcdef0123456789abcdef0123456789",
|
|
43
|
+
teamName: input.teamName || "TestTeam",
|
|
44
|
+
roomName: input.roomName || "TestRoom",
|
|
45
|
+
writer: {
|
|
46
|
+
id: "12345",
|
|
47
|
+
name: input.writerName || "TestUser",
|
|
48
|
+
email: input.writerEmail || "test@example.com",
|
|
49
|
+
phoneNumber: "+82-10-1234-5678"
|
|
50
|
+
},
|
|
51
|
+
text: input.text,
|
|
52
|
+
keyword: input.keyword || "test",
|
|
53
|
+
createdAt: now,
|
|
54
|
+
data: {},
|
|
55
|
+
platform: "web",
|
|
56
|
+
ip: "127.0.0.1"
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
success: true,
|
|
60
|
+
data: {
|
|
61
|
+
webhookType: 'team-outgoing',
|
|
62
|
+
payload,
|
|
63
|
+
payloadJson: JSON.stringify(payload, null, 2),
|
|
64
|
+
curlCommand: this.generateCurlCommand(payload),
|
|
65
|
+
message: "Team Outgoing Webhook test payload generated. Use curlCommand to test your handler."
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const payload = {
|
|
70
|
+
token: "abcdef0123456789abcdef0123456789",
|
|
71
|
+
teamName: input.teamName || "TestTeam",
|
|
72
|
+
roomName: input.roomName || "TestRoom",
|
|
73
|
+
writerName: input.writerName || "TestUser",
|
|
74
|
+
writerEmail: input.writerEmail || "test@example.com",
|
|
75
|
+
text: input.text,
|
|
76
|
+
keyword: input.keyword || "test",
|
|
77
|
+
createdAt: now,
|
|
78
|
+
data: {},
|
|
79
|
+
platform: "web",
|
|
80
|
+
ip: "127.0.0.1"
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
success: true,
|
|
84
|
+
data: {
|
|
85
|
+
webhookType: 'outgoing',
|
|
86
|
+
payload,
|
|
87
|
+
payloadJson: JSON.stringify(payload, null, 2),
|
|
88
|
+
curlCommand: this.generateCurlCommand(payload),
|
|
89
|
+
message: "Outgoing Webhook test payload generated. Use curlCommand to test your handler."
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
return { success: false, error: `Error generating payload: ${error}` };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
generateCurlCommand(payload) {
|
|
98
|
+
return `curl -X POST http://localhost:3000/webhook \\
|
|
99
|
+
-H "Content-Type: application/json" \\
|
|
100
|
+
-d '${JSON.stringify(payload)}'`;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export default SimulateOutgoingPayloadTool;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { MCPTool } from "mcp-framework";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { validateHexColor } from "../../utils/validateColor.js";
|
|
4
|
+
class ValidateOutgoingResponseTool extends MCPTool {
|
|
5
|
+
name = "validate_outgoing_response";
|
|
6
|
+
description = "Validate a Jandi Outgoing Webhook response format. Checks body length, color format, connectInfo structure, and total size limits.";
|
|
7
|
+
schema = {
|
|
8
|
+
body: {
|
|
9
|
+
type: z.string(),
|
|
10
|
+
description: "The response body text to validate",
|
|
11
|
+
},
|
|
12
|
+
connectColor: {
|
|
13
|
+
type: z.string().optional(),
|
|
14
|
+
description: "Hex color code to validate (e.g., '#FF0000')",
|
|
15
|
+
},
|
|
16
|
+
connectInfo: {
|
|
17
|
+
type: z.array(z.object({
|
|
18
|
+
title: z.string().optional(),
|
|
19
|
+
description: z.string().optional(),
|
|
20
|
+
imageUrl: z.string().optional(),
|
|
21
|
+
})).optional(),
|
|
22
|
+
description: "Array of connectInfo sections to validate",
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
async execute(input) {
|
|
26
|
+
const MAX_BODY_LENGTH = 5000;
|
|
27
|
+
const MAX_DATA_SIZE = 256 * 1024;
|
|
28
|
+
const issues = [];
|
|
29
|
+
const warnings = [];
|
|
30
|
+
// Validate body
|
|
31
|
+
if (!input.body || input.body.trim().length === 0) {
|
|
32
|
+
issues.push("body is required and cannot be empty");
|
|
33
|
+
}
|
|
34
|
+
else if (input.body.length > MAX_BODY_LENGTH) {
|
|
35
|
+
issues.push(`body exceeds maximum length of ${MAX_BODY_LENGTH} characters (current: ${input.body.length})`);
|
|
36
|
+
}
|
|
37
|
+
// Validate color format
|
|
38
|
+
if (input.connectColor) {
|
|
39
|
+
const colorResult = validateHexColor(input.connectColor);
|
|
40
|
+
if (!colorResult.valid) {
|
|
41
|
+
issues.push(`connectColor: ${colorResult.error}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Validate connectInfo
|
|
45
|
+
if (input.connectInfo) {
|
|
46
|
+
if (!Array.isArray(input.connectInfo)) {
|
|
47
|
+
issues.push("connectInfo must be an array");
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
input.connectInfo.forEach((info, index) => {
|
|
51
|
+
if (!info.title && !info.description && !info.imageUrl) {
|
|
52
|
+
warnings.push(`connectInfo[${index}] has no title, description, or imageUrl - it will be empty`);
|
|
53
|
+
}
|
|
54
|
+
if (info.imageUrl && !info.imageUrl.match(/^https?:\/\/.+/)) {
|
|
55
|
+
issues.push(`connectInfo[${index}].imageUrl must be a valid HTTP/HTTPS URL`);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Validate total data size
|
|
61
|
+
const response = {
|
|
62
|
+
body: input.body,
|
|
63
|
+
connectColor: input.connectColor,
|
|
64
|
+
connectInfo: input.connectInfo
|
|
65
|
+
};
|
|
66
|
+
const dataSize = JSON.stringify(response).length;
|
|
67
|
+
if (dataSize > MAX_DATA_SIZE) {
|
|
68
|
+
issues.push(`Total response data exceeds maximum size of ${MAX_DATA_SIZE} bytes (current: ${dataSize})`);
|
|
69
|
+
}
|
|
70
|
+
const isValid = issues.length === 0;
|
|
71
|
+
return {
|
|
72
|
+
success: true,
|
|
73
|
+
data: {
|
|
74
|
+
valid: isValid,
|
|
75
|
+
message: isValid
|
|
76
|
+
? "Outgoing webhook response format is valid"
|
|
77
|
+
: "Outgoing webhook response has validation issues",
|
|
78
|
+
issues: issues.length > 0 ? issues : undefined,
|
|
79
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
80
|
+
stats: {
|
|
81
|
+
bodyLength: input.body?.length || 0,
|
|
82
|
+
maxBodyLength: MAX_BODY_LENGTH,
|
|
83
|
+
totalDataSize: dataSize,
|
|
84
|
+
maxDataSize: MAX_DATA_SIZE,
|
|
85
|
+
connectInfoSections: input.connectInfo?.length || 0
|
|
86
|
+
},
|
|
87
|
+
validResponseFormat: {
|
|
88
|
+
body: "string (required, max 5000 chars)",
|
|
89
|
+
connectColor: "string (optional, hex color e.g. '#FAC11B')",
|
|
90
|
+
connectInfo: "array (optional, [{title?, description?, imageUrl?}])"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export default ValidateOutgoingResponseTool;
|