@tuteliq/mcp 3.2.0 → 3.2.2

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.
@@ -1,20 +1,17 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.registerDetectionTools = registerDetectionTools;
4
- const zod_1 = require("zod");
5
- const server_1 = require("@modelcontextprotocol/ext-apps/server");
6
- const fs_1 = require("fs");
7
- const path_1 = require("path");
8
- const formatters_js_1 = require("../formatters.js");
1
+ import { z } from 'zod';
2
+ import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/server';
3
+ import { readFileSync } from 'fs';
4
+ import { resolve } from 'path';
5
+ import { severityEmoji, riskEmoji } from '../formatters.js';
9
6
  const DETECTION_WIDGET_URI = 'ui://tuteliq/detection-result.html';
10
7
  function loadWidget(name) {
11
- return (0, fs_1.readFileSync)((0, path_1.resolve)(__dirname, '../../dist-ui', name), 'utf-8');
8
+ return readFileSync(resolve(__dirname, '../../dist-ui', name), 'utf-8');
12
9
  }
13
- const contextSchema = zod_1.z.object({
14
- language: zod_1.z.string().optional(),
15
- ageGroup: zod_1.z.string().optional(),
16
- relationship: zod_1.z.string().optional(),
17
- platform: zod_1.z.string().optional(),
10
+ const contextSchema = z.object({
11
+ language: z.string().optional(),
12
+ ageGroup: z.string().optional(),
13
+ relationship: z.string().optional(),
14
+ platform: z.string().optional(),
18
15
  }).optional();
19
16
  const uiMeta = (desc, invoking, invoked) => ({
20
17
  ui: { resourceUri: DETECTION_WIDGET_URI },
@@ -22,22 +19,22 @@ const uiMeta = (desc, invoking, invoked) => ({
22
19
  'openai/toolInvocation/invoking': invoking,
23
20
  'openai/toolInvocation/invoked': invoked,
24
21
  });
25
- function registerDetectionTools(server, client) {
22
+ export function registerDetectionTools(server, client) {
26
23
  // Cast needed due to CJS/ESM dual-module type mismatch between MCP SDK and ext-apps
27
- (0, server_1.registerAppResource)(server, DETECTION_WIDGET_URI, DETECTION_WIDGET_URI, { mimeType: server_1.RESOURCE_MIME_TYPE }, async () => ({
24
+ registerAppResource(server, DETECTION_WIDGET_URI, DETECTION_WIDGET_URI, { mimeType: RESOURCE_MIME_TYPE }, async () => ({
28
25
  contents: [{
29
26
  uri: DETECTION_WIDGET_URI,
30
- mimeType: server_1.RESOURCE_MIME_TYPE,
27
+ mimeType: RESOURCE_MIME_TYPE,
31
28
  text: loadWidget('detection-result.html'),
32
29
  }],
33
30
  }));
34
31
  // ── detect_bullying ────────────────────────────────────────────────────────
35
- (0, server_1.registerAppTool)(server, 'detect_bullying', {
32
+ registerAppTool(server, 'detect_bullying', {
36
33
  title: 'Detect Bullying',
37
34
  description: 'Analyze text content to detect bullying, harassment, or harmful language.',
38
35
  annotations: { readOnlyHint: true, openWorldHint: true, destructiveHint: false },
39
36
  inputSchema: {
40
- content: zod_1.z.string().describe('The text content to analyze for bullying'),
37
+ content: z.string().describe('The text content to analyze for bullying'),
41
38
  context: contextSchema,
42
39
  },
43
40
  _meta: uiMeta('Shows bullying detection results with risk indicators', 'Analyzing content for bullying...', 'Bullying analysis complete.'),
@@ -46,7 +43,7 @@ function registerDetectionTools(server, client) {
46
43
  content,
47
44
  context: context,
48
45
  });
49
- const emoji = formatters_js_1.severityEmoji[result.severity] || '\u26AA';
46
+ const emoji = severityEmoji[result.severity] || '\u26AA';
50
47
  const text = `## ${result.is_bullying ? '\u26A0\uFE0F Bullying Detected' : '\u2705 No Bullying Detected'}
51
48
 
52
49
  **Severity:** ${emoji} ${result.severity.charAt(0).toUpperCase() + result.severity.slice(1)}
@@ -66,16 +63,16 @@ ${result.rationale}
66
63
  };
67
64
  });
68
65
  // ── detect_grooming ────────────────────────────────────────────────────────
69
- (0, server_1.registerAppTool)(server, 'detect_grooming', {
66
+ registerAppTool(server, 'detect_grooming', {
70
67
  title: 'Detect Grooming',
71
68
  description: 'Analyze a conversation for grooming patterns and predatory behavior.',
72
69
  annotations: { readOnlyHint: true, openWorldHint: true, destructiveHint: false },
73
70
  inputSchema: {
74
- messages: zod_1.z.array(zod_1.z.object({
75
- role: zod_1.z.enum(['adult', 'child', 'unknown']),
76
- content: zod_1.z.string(),
71
+ messages: z.array(z.object({
72
+ role: z.enum(['adult', 'child', 'unknown']),
73
+ content: z.string(),
77
74
  })).describe('Array of messages in the conversation'),
78
- childAge: zod_1.z.number().optional().describe('Age of the child in the conversation'),
75
+ childAge: z.number().optional().describe('Age of the child in the conversation'),
79
76
  },
80
77
  _meta: uiMeta('Shows grooming detection results with risk indicators', 'Analyzing conversation for grooming patterns...', 'Grooming analysis complete.'),
81
78
  }, async ({ messages, childAge }) => {
@@ -83,7 +80,7 @@ ${result.rationale}
83
80
  messages,
84
81
  childAge,
85
82
  });
86
- const emoji = formatters_js_1.riskEmoji[result.grooming_risk] || '\u26AA';
83
+ const emoji = riskEmoji[result.grooming_risk] || '\u26AA';
87
84
  const text = `## ${result.grooming_risk === 'none' ? '\u2705 No Grooming Detected' : '\u26A0\uFE0F Grooming Risk Detected'}
88
85
 
89
86
  **Risk Level:** ${emoji} ${result.grooming_risk.charAt(0).toUpperCase() + result.grooming_risk.slice(1)}
@@ -103,12 +100,12 @@ ${result.rationale}
103
100
  };
104
101
  });
105
102
  // ── detect_unsafe ──────────────────────────────────────────────────────────
106
- (0, server_1.registerAppTool)(server, 'detect_unsafe', {
103
+ registerAppTool(server, 'detect_unsafe', {
107
104
  title: 'Detect Unsafe Content',
108
105
  description: 'Detect unsafe content including self-harm, violence, drugs, explicit material.',
109
106
  annotations: { readOnlyHint: true, openWorldHint: true, destructiveHint: false },
110
107
  inputSchema: {
111
- content: zod_1.z.string().describe('The text content to analyze for unsafe content'),
108
+ content: z.string().describe('The text content to analyze for unsafe content'),
112
109
  context: contextSchema,
113
110
  },
114
111
  _meta: uiMeta('Shows unsafe content detection results', 'Analyzing content for safety concerns...', 'Safety analysis complete.'),
@@ -117,7 +114,7 @@ ${result.rationale}
117
114
  content,
118
115
  context: context,
119
116
  });
120
- const emoji = formatters_js_1.severityEmoji[result.severity] || '\u26AA';
117
+ const emoji = severityEmoji[result.severity] || '\u26AA';
121
118
  const text = `## ${result.unsafe ? '\u26A0\uFE0F Unsafe Content Detected' : '\u2705 Content is Safe'}
122
119
 
123
120
  **Severity:** ${emoji} ${result.severity.charAt(0).toUpperCase() + result.severity.slice(1)}
@@ -137,18 +134,18 @@ ${result.rationale}
137
134
  };
138
135
  });
139
136
  // ── analyze (quick combined) ───────────────────────────────────────────────
140
- (0, server_1.registerAppTool)(server, 'analyze', {
137
+ registerAppTool(server, 'analyze', {
141
138
  title: 'Quick Safety Analysis',
142
139
  description: 'Quick comprehensive safety analysis that checks for both bullying and unsafe content.',
143
140
  annotations: { readOnlyHint: true, openWorldHint: true, destructiveHint: false },
144
141
  inputSchema: {
145
- content: zod_1.z.string().describe('The text content to analyze'),
146
- include: zod_1.z.array(zod_1.z.enum(['bullying', 'unsafe'])).optional().describe('Which checks to run (default: both)'),
142
+ content: z.string().describe('The text content to analyze'),
143
+ include: z.array(z.enum(['bullying', 'unsafe'])).optional().describe('Which checks to run (default: both)'),
147
144
  },
148
145
  _meta: uiMeta('Shows combined safety analysis results', 'Running safety analysis...', 'Safety analysis complete.'),
149
146
  }, async ({ content, include }) => {
150
147
  const result = await client.analyze({ content, include });
151
- const emoji = formatters_js_1.riskEmoji[result.risk_level] || '\u26AA';
148
+ const emoji = riskEmoji[result.risk_level] || '\u26AA';
152
149
  const text = `## Safety Analysis Results
153
150
 
154
151
  **Overall Risk:** ${emoji} ${result.risk_level.charAt(0).toUpperCase() + result.risk_level.slice(1)}
@@ -1,9 +1,6 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.registerFraudTools = registerFraudTools;
4
- const zod_1 = require("zod");
5
- const server_1 = require("@modelcontextprotocol/ext-apps/server");
6
- const formatters_js_1 = require("../formatters.js");
1
+ import { z } from 'zod';
2
+ import { registerAppTool } from '@modelcontextprotocol/ext-apps/server';
3
+ import { formatDetectionResult } from '../formatters.js';
7
4
  const DETECTION_WIDGET_URI = 'ui://tuteliq/detection-result.html';
8
5
  const FRAUD_TOOLS = [
9
6
  {
@@ -72,15 +69,15 @@ const FRAUD_TOOLS = [
72
69
  },
73
70
  ];
74
71
  const fraudInputSchema = {
75
- content: zod_1.z.string().describe('Text content to analyze'),
76
- context: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional().describe('Optional analysis context'),
77
- include_evidence: zod_1.z.boolean().optional().describe('Include supporting evidence excerpts'),
78
- external_id: zod_1.z.string().optional().describe('External tracking ID'),
79
- customer_id: zod_1.z.string().optional().describe('Customer identifier'),
72
+ content: z.string().describe('Text content to analyze'),
73
+ context: z.record(z.string(), z.unknown()).optional().describe('Optional analysis context'),
74
+ include_evidence: z.boolean().optional().describe('Include supporting evidence excerpts'),
75
+ external_id: z.string().optional().describe('External tracking ID'),
76
+ customer_id: z.string().optional().describe('Customer identifier'),
80
77
  };
81
- function registerFraudTools(server, client) {
78
+ export function registerFraudTools(server, client) {
82
79
  for (const tool of FRAUD_TOOLS) {
83
- (0, server_1.registerAppTool)(server, tool.name, {
80
+ registerAppTool(server, tool.name, {
84
81
  title: tool.title,
85
82
  description: tool.description,
86
83
  annotations: { readOnlyHint: true, openWorldHint: true, destructiveHint: false },
@@ -102,7 +99,7 @@ function registerFraudTools(server, client) {
102
99
  });
103
100
  return {
104
101
  structuredContent: { toolName: tool.name, result, branding: { appName: 'Tuteliq' } },
105
- content: [{ type: 'text', text: (0, formatters_js_1.formatDetectionResult)(result) }],
102
+ content: [{ type: 'text', text: formatDetectionResult(result) }],
106
103
  };
107
104
  });
108
105
  }
@@ -1,36 +1,33 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.registerMediaTools = registerMediaTools;
4
- const zod_1 = require("zod");
5
- const server_1 = require("@modelcontextprotocol/ext-apps/server");
6
- const fs_1 = require("fs");
7
- const path_1 = require("path");
8
- const formatters_js_1 = require("../formatters.js");
1
+ import { z } from 'zod';
2
+ import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/server';
3
+ import { readFileSync } from 'fs';
4
+ import { resolve } from 'path';
5
+ import { severityEmoji, trendEmoji, formatVideoResult } from '../formatters.js';
9
6
  const MEDIA_WIDGET_URI = 'ui://tuteliq/media-result.html';
10
7
  function loadWidget(name) {
11
- return (0, fs_1.readFileSync)((0, path_1.resolve)(__dirname, '../../dist-ui', name), 'utf-8');
8
+ return readFileSync(resolve(__dirname, '../../dist-ui', name), 'utf-8');
12
9
  }
13
10
  function filenameFromPath(filePath) {
14
11
  return filePath.split('/').pop() || filePath;
15
12
  }
16
- function registerMediaTools(server, client) {
17
- (0, server_1.registerAppResource)(server, MEDIA_WIDGET_URI, MEDIA_WIDGET_URI, { mimeType: server_1.RESOURCE_MIME_TYPE }, async () => ({
13
+ export function registerMediaTools(server, client) {
14
+ registerAppResource(server, MEDIA_WIDGET_URI, MEDIA_WIDGET_URI, { mimeType: RESOURCE_MIME_TYPE }, async () => ({
18
15
  contents: [{
19
16
  uri: MEDIA_WIDGET_URI,
20
- mimeType: server_1.RESOURCE_MIME_TYPE,
17
+ mimeType: RESOURCE_MIME_TYPE,
21
18
  text: loadWidget('media-result.html'),
22
19
  }],
23
20
  }));
24
21
  // ── analyze_voice ──────────────────────────────────────────────────────────
25
- (0, server_1.registerAppTool)(server, 'analyze_voice', {
22
+ registerAppTool(server, 'analyze_voice', {
26
23
  title: 'Analyze Voice',
27
24
  description: 'Analyze an audio file for safety concerns. Transcribes the audio via Whisper, then runs safety analysis on the transcript. Supports mp3, wav, m4a, ogg, flac, webm, mp4.',
28
25
  annotations: { readOnlyHint: true, openWorldHint: true, destructiveHint: false },
29
26
  inputSchema: {
30
- file_path: zod_1.z.string().describe('Absolute path to the audio file on disk'),
31
- analysis_type: zod_1.z.enum(['bullying', 'unsafe', 'grooming', 'emotions', 'all']).optional().describe('Type of analysis to run on the transcript (default: all)'),
32
- child_age: zod_1.z.number().optional().describe('Child age (used for grooming analysis)'),
33
- language: zod_1.z.string().optional().describe('Language hint for transcription (e.g., "en", "es")'),
27
+ file_path: z.string().describe('Absolute path to the audio file on disk'),
28
+ analysis_type: z.enum(['bullying', 'unsafe', 'grooming', 'emotions', 'all']).optional().describe('Type of analysis to run on the transcript (default: all)'),
29
+ child_age: z.number().optional().describe('Child age (used for grooming analysis)'),
30
+ language: z.string().optional().describe('Language hint for transcription (e.g., "en", "es")'),
34
31
  },
35
32
  _meta: {
36
33
  ui: { resourceUri: MEDIA_WIDGET_URI },
@@ -39,7 +36,7 @@ function registerMediaTools(server, client) {
39
36
  'openai/toolInvocation/invoked': 'Voice analysis complete.',
40
37
  },
41
38
  }, async ({ file_path, analysis_type, child_age, language }) => {
42
- const buffer = (0, fs_1.readFileSync)(file_path);
39
+ const buffer = readFileSync(file_path);
43
40
  const filename = filenameFromPath(file_path);
44
41
  const result = await client.analyzeVoice({
45
42
  file: buffer,
@@ -48,7 +45,7 @@ function registerMediaTools(server, client) {
48
45
  language,
49
46
  childAge: child_age,
50
47
  });
51
- const emoji = formatters_js_1.severityEmoji[result.overall_severity] || '\u2705';
48
+ const emoji = severityEmoji[result.overall_severity] || '\u2705';
52
49
  const segmentLines = result.transcription.segments
53
50
  .slice(0, 20)
54
51
  .map(s => `\`${s.start.toFixed(1)}s\u2013${s.end.toFixed(1)}s\` ${s.text}`)
@@ -64,7 +61,7 @@ function registerMediaTools(server, client) {
64
61
  analysisLines.push(`**Grooming:** ${result.analysis.grooming.grooming_risk !== 'none' ? '\u26A0\uFE0F ' + result.analysis.grooming.grooming_risk : '\u2705 Clear'} (${(result.analysis.grooming.risk_score * 100).toFixed(0)}%)`);
65
62
  }
66
63
  if (result.analysis.emotions) {
67
- analysisLines.push(`**Emotions:** ${result.analysis.emotions.dominant_emotions.join(', ')} (${formatters_js_1.trendEmoji[result.analysis.emotions.trend] || ''} ${result.analysis.emotions.trend})`);
64
+ analysisLines.push(`**Emotions:** ${result.analysis.emotions.dominant_emotions.join(', ')} (${trendEmoji[result.analysis.emotions.trend] || ''} ${result.analysis.emotions.trend})`);
68
65
  }
69
66
  const text = `## \u{1F399}\uFE0F Voice Analysis
70
67
 
@@ -87,13 +84,13 @@ ${analysisLines.join('\n')}`;
87
84
  };
88
85
  });
89
86
  // ── analyze_image ──────────────────────────────────────────────────────────
90
- (0, server_1.registerAppTool)(server, 'analyze_image', {
87
+ registerAppTool(server, 'analyze_image', {
91
88
  title: 'Analyze Image',
92
89
  description: 'Analyze an image for visual safety concerns and OCR text extraction. Supports png, jpg, jpeg, gif, webp.',
93
90
  annotations: { readOnlyHint: true, openWorldHint: true, destructiveHint: false },
94
91
  inputSchema: {
95
- file_path: zod_1.z.string().describe('Absolute path to the image file on disk'),
96
- analysis_type: zod_1.z.enum(['bullying', 'unsafe', 'emotions', 'all']).optional().describe('Type of analysis to run on extracted text (default: all)'),
92
+ file_path: z.string().describe('Absolute path to the image file on disk'),
93
+ analysis_type: z.enum(['bullying', 'unsafe', 'emotions', 'all']).optional().describe('Type of analysis to run on extracted text (default: all)'),
97
94
  },
98
95
  _meta: {
99
96
  ui: { resourceUri: MEDIA_WIDGET_URI },
@@ -102,14 +99,14 @@ ${analysisLines.join('\n')}`;
102
99
  'openai/toolInvocation/invoked': 'Image analysis complete.',
103
100
  },
104
101
  }, async ({ file_path, analysis_type }) => {
105
- const buffer = (0, fs_1.readFileSync)(file_path);
102
+ const buffer = readFileSync(file_path);
106
103
  const filename = filenameFromPath(file_path);
107
104
  const result = await client.analyzeImage({
108
105
  file: buffer,
109
106
  filename,
110
107
  analysisType: analysis_type || 'all',
111
108
  });
112
- const emoji = formatters_js_1.severityEmoji[result.overall_severity] || '\u2705';
109
+ const emoji = severityEmoji[result.overall_severity] || '\u2705';
113
110
  const textAnalysisLines = [];
114
111
  if (result.text_analysis?.bullying) {
115
112
  textAnalysisLines.push(`**Bullying:** ${result.text_analysis.bullying.is_bullying ? '\u26A0\uFE0F Detected' : '\u2705 Clear'} (${(result.text_analysis.bullying.risk_score * 100).toFixed(0)}%)`);
@@ -127,7 +124,7 @@ ${analysisLines.join('\n')}`;
127
124
 
128
125
  ### Vision Results
129
126
  **Description:** ${result.vision.visual_description}
130
- **Visual Severity:** ${formatters_js_1.severityEmoji[result.vision.visual_severity] || '\u2705'} ${result.vision.visual_severity}
127
+ **Visual Severity:** ${severityEmoji[result.vision.visual_severity] || '\u2705'} ${result.vision.visual_severity}
131
128
  **Visual Confidence:** ${(result.vision.visual_confidence * 100).toFixed(0)}%
132
129
  **Contains Text:** ${result.vision.contains_text ? 'Yes' : 'No'}
133
130
  **Contains Faces:** ${result.vision.contains_faces ? 'Yes' : 'No'}
@@ -142,13 +139,13 @@ ${textAnalysisLines.length > 0 ? `### Text Analysis Results\n${textAnalysisLines
142
139
  };
143
140
  });
144
141
  // ── analyze_video ──────────────────────────────────────────────────────────
145
- (0, server_1.registerAppTool)(server, 'analyze_video', {
142
+ registerAppTool(server, 'analyze_video', {
146
143
  title: 'Analyze Video',
147
144
  description: 'Analyze a video file for safety concerns. Extracts key frames and runs safety classification. Supports mp4, mov, avi, webm, mkv.',
148
145
  annotations: { readOnlyHint: true, openWorldHint: true, destructiveHint: false },
149
146
  inputSchema: {
150
- file_path: zod_1.z.string().describe('Absolute path to the video file on disk'),
151
- age_group: zod_1.z.string().optional().describe('Age group for calibrated analysis (e.g., "child", "teen", "adult")'),
147
+ file_path: z.string().describe('Absolute path to the video file on disk'),
148
+ age_group: z.string().optional().describe('Age group for calibrated analysis (e.g., "child", "teen", "adult")'),
152
149
  },
153
150
  _meta: {
154
151
  ui: { resourceUri: MEDIA_WIDGET_URI },
@@ -157,7 +154,7 @@ ${textAnalysisLines.length > 0 ? `### Text Analysis Results\n${textAnalysisLines
157
154
  'openai/toolInvocation/invoked': 'Video analysis complete.',
158
155
  },
159
156
  }, async ({ file_path, age_group }) => {
160
- const buffer = (0, fs_1.readFileSync)(file_path);
157
+ const buffer = readFileSync(file_path);
161
158
  const filename = filenameFromPath(file_path);
162
159
  const result = await client.analyzeVideo({
163
160
  file: buffer,
@@ -166,7 +163,7 @@ ${textAnalysisLines.length > 0 ? `### Text Analysis Results\n${textAnalysisLines
166
163
  });
167
164
  return {
168
165
  structuredContent: { toolName: 'analyze_video', result, branding: { appName: 'Tuteliq' } },
169
- content: [{ type: 'text', text: (0, formatters_js_1.formatVideoResult)(result) }],
166
+ content: [{ type: 'text', text: formatVideoResult(result) }],
170
167
  };
171
168
  });
172
169
  }
@@ -3,5 +3,10 @@ import type { Request, Response } from 'express';
3
3
  export type TransportMode = 'http' | 'stdio';
4
4
  export declare function getTransportMode(): TransportMode;
5
5
  export declare function startStdio(server: McpServer): Promise<void>;
6
- export declare function createHttpHandler(server: McpServer): (req: Request, res: Response) => Promise<void>;
6
+ export declare function createHttpHandlers(createServer: () => McpServer): {
7
+ streamableHandler: (req: Request, res: Response) => Promise<void>;
8
+ sseHandler: (req: Request, res: Response) => Promise<void>;
9
+ messagesHandler: (req: Request, res: Response) => Promise<void>;
10
+ closeAll: () => Promise<void>;
11
+ };
7
12
  //# sourceMappingURL=transport.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/transport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAGpE,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEjD,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,OAAO,CAAC;AAE7C,wBAAgB,gBAAgB,IAAI,aAAa,CAIhD;AAED,wBAAsB,UAAU,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAIjE;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,IAGnC,KAAK,OAAO,EAAE,KAAK,QAAQ,KAAG,OAAO,CAAC,IAAI,CAAC,CAqC1D"}
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/transport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAKpE,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEjD,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,OAAO,CAAC;AAU7C,wBAAgB,gBAAgB,IAAI,aAAa,CAIhD;AAED,wBAAsB,UAAU,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAIjE;AAED,wBAAgB,kBAAkB,CAAC,YAAY,EAAE,MAAM,SAAS;6BAmCxB,OAAO,OAAO,QAAQ,KAAG,OAAO,CAAC,IAAI,CAAC;sBA6E7C,OAAO,OAAO,QAAQ,KAAG,OAAO,CAAC,IAAI,CAAC;2BAajC,OAAO,OAAO,QAAQ,KAAG,OAAO,CAAC,IAAI,CAAC;oBAa/C,OAAO,CAAC,IAAI,CAAC;EAazC"}
@@ -1,53 +1,119 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getTransportMode = getTransportMode;
4
- exports.startStdio = startStdio;
5
- exports.createHttpHandler = createHttpHandler;
6
- const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
7
- const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
8
- function getTransportMode() {
1
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
2
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
3
+ import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
4
+ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
5
+ const SESSION_TTL_MS = 30 * 60 * 1000; // 30 minutes
6
+ const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // check every 5 minutes
7
+ export function getTransportMode() {
9
8
  const env = process.env.TUTELIQ_MCP_TRANSPORT?.toLowerCase();
10
9
  if (env === 'stdio')
11
10
  return 'stdio';
12
11
  return 'http';
13
12
  }
14
- async function startStdio(server) {
15
- const transport = new stdio_js_1.StdioServerTransport();
13
+ export async function startStdio(server) {
14
+ const transport = new StdioServerTransport();
16
15
  await server.connect(transport);
17
16
  console.error('Tuteliq MCP server running on stdio');
18
17
  }
19
- function createHttpHandler(server) {
20
- const transports = new Map();
21
- return async (req, res) => {
18
+ export function createHttpHandlers(createServer) {
19
+ const sessions = new Map();
20
+ function addSession(id, transport) {
21
+ sessions.set(id, { transport, lastActivity: Date.now() });
22
+ }
23
+ function removeSession(id) {
24
+ sessions.delete(id);
25
+ }
26
+ function getTransport(id) {
27
+ const entry = sessions.get(id);
28
+ if (entry) {
29
+ entry.lastActivity = Date.now();
30
+ return entry.transport;
31
+ }
32
+ return undefined;
33
+ }
34
+ // Periodic cleanup of stale sessions
35
+ const cleanupTimer = setInterval(async () => {
36
+ const now = Date.now();
37
+ for (const [id, entry] of sessions) {
38
+ if (now - entry.lastActivity > SESSION_TTL_MS) {
39
+ try {
40
+ await entry.transport.close();
41
+ }
42
+ catch { /* already closed */ }
43
+ sessions.delete(id);
44
+ }
45
+ }
46
+ }, CLEANUP_INTERVAL_MS);
47
+ cleanupTimer.unref();
48
+ // Streamable HTTP handler — POST /mcp, GET /mcp, DELETE /mcp
49
+ const streamableHandler = async (req, res) => {
22
50
  const sessionId = req.headers['mcp-session-id'];
23
51
  if (req.method === 'POST') {
24
- const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
25
- sessionIdGenerator: () => crypto.randomUUID(),
26
- onsessioninitialized: (id) => {
27
- transports.set(id, transport);
28
- },
52
+ if (sessionId) {
53
+ const existing = getTransport(sessionId);
54
+ if (existing) {
55
+ if (existing instanceof StreamableHTTPServerTransport) {
56
+ await existing.handleRequest(req, res, req.body);
57
+ return;
58
+ }
59
+ res.status(400).json({
60
+ jsonrpc: '2.0',
61
+ error: { code: -32000, message: 'Session uses a different transport protocol' },
62
+ id: null,
63
+ });
64
+ return;
65
+ }
66
+ }
67
+ if (!sessionId && isInitializeRequest(req.body)) {
68
+ const transport = new StreamableHTTPServerTransport({
69
+ sessionIdGenerator: () => crypto.randomUUID(),
70
+ onsessioninitialized: (id) => {
71
+ addSession(id, transport);
72
+ },
73
+ });
74
+ transport.onclose = () => {
75
+ const sid = transport.sessionId;
76
+ if (sid)
77
+ removeSession(sid);
78
+ };
79
+ const server = createServer();
80
+ await server.connect(transport);
81
+ await transport.handleRequest(req, res, req.body);
82
+ return;
83
+ }
84
+ res.status(400).json({
85
+ jsonrpc: '2.0',
86
+ error: { code: -32000, message: 'Bad Request: No valid session ID provided' },
87
+ id: null,
29
88
  });
30
- transport.onclose = () => {
31
- const id = [...transports.entries()].find(([, t]) => t === transport)?.[0];
32
- if (id)
33
- transports.delete(id);
34
- };
35
- await server.connect(transport);
36
- await transport.handleRequest(req, res, req.body);
37
89
  }
38
90
  else if (req.method === 'GET') {
39
- if (!sessionId || !transports.has(sessionId)) {
91
+ if (!sessionId) {
40
92
  res.status(400).json({ error: 'Invalid or missing session ID' });
41
93
  return;
42
94
  }
43
- const transport = transports.get(sessionId);
44
- await transport.handleRequest(req, res);
95
+ const transport = getTransport(sessionId);
96
+ if (!transport) {
97
+ res.status(400).json({ error: 'Invalid or missing session ID' });
98
+ return;
99
+ }
100
+ if (transport instanceof StreamableHTTPServerTransport) {
101
+ await transport.handleRequest(req, res);
102
+ }
103
+ else {
104
+ res.status(400).json({ error: 'Session uses a different transport protocol' });
105
+ }
45
106
  }
46
107
  else if (req.method === 'DELETE') {
47
- if (sessionId && transports.has(sessionId)) {
48
- const transport = transports.get(sessionId);
49
- await transport.handleRequest(req, res);
50
- transports.delete(sessionId);
108
+ if (sessionId) {
109
+ const transport = getTransport(sessionId);
110
+ if (transport && transport instanceof StreamableHTTPServerTransport) {
111
+ await transport.handleRequest(req, res);
112
+ removeSession(sessionId);
113
+ }
114
+ else {
115
+ res.status(400).json({ error: 'Invalid or missing session ID' });
116
+ }
51
117
  }
52
118
  else {
53
119
  res.status(400).json({ error: 'Invalid or missing session ID' });
@@ -57,4 +123,38 @@ function createHttpHandler(server) {
57
123
  res.status(405).json({ error: 'Method not allowed' });
58
124
  }
59
125
  };
126
+ // Legacy SSE handler — GET /sse
127
+ const sseHandler = async (req, res) => {
128
+ const transport = new SSEServerTransport('/messages', res);
129
+ addSession(transport.sessionId, transport);
130
+ res.on('close', () => {
131
+ removeSession(transport.sessionId);
132
+ });
133
+ const server = createServer();
134
+ await server.connect(transport);
135
+ };
136
+ // Legacy message handler — POST /messages?sessionId=...
137
+ const messagesHandler = async (req, res) => {
138
+ const sessionId = req.query.sessionId;
139
+ const transport = getTransport(sessionId);
140
+ if (!transport || !(transport instanceof SSEServerTransport)) {
141
+ res.status(400).json({ error: 'Invalid or missing session ID' });
142
+ return;
143
+ }
144
+ await transport.handlePostMessage(req, res, req.body);
145
+ };
146
+ // Graceful shutdown
147
+ const closeAll = async () => {
148
+ clearInterval(cleanupTimer);
149
+ for (const [id, entry] of sessions) {
150
+ try {
151
+ await entry.transport.close();
152
+ sessions.delete(id);
153
+ }
154
+ catch (error) {
155
+ console.error(`Error closing transport ${id}:`, error);
156
+ }
157
+ }
158
+ };
159
+ return { streamableHandler, sseHandler, messagesHandler, closeAll };
60
160
  }
package/package.json CHANGED
@@ -1,25 +1,33 @@
1
1
  {
2
2
  "name": "@tuteliq/mcp",
3
- "version": "3.2.0",
3
+ "version": "3.2.2",
4
4
  "description": "MCP App server for Tuteliq - AI-powered child safety analysis with interactive UI widgets for Claude, ChatGPT, and other MCP hosts",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
5
+ "type": "module",
6
+ "main": "./dist/src/index.js",
7
+ "types": "./dist/src/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/src/index.js",
11
+ "types": "./dist/src/index.d.ts"
12
+ }
13
+ },
7
14
  "bin": {
8
- "tuteliq-mcp": "./dist/index.js"
15
+ "tuteliq-mcp": "./dist/src/index.js"
9
16
  },
10
17
  "files": [
11
18
  "dist",
12
19
  "dist-ui"
13
20
  ],
14
21
  "scripts": {
22
+ "clean": "rm -rf dist",
15
23
  "build:server": "tsc",
16
24
  "build:ui": "rm -rf dist-ui && WIDGET=detection-result vite build && WIDGET=multi-result vite build && WIDGET=emotions-result vite build && WIDGET=media-result vite build && WIDGET=action-plan vite build && WIDGET=report-result vite build",
17
25
  "build": "npm run build:ui && npm run build:server",
18
26
  "dev:server": "tsx watch server.ts",
19
27
  "dev:ui": "vite build --watch",
20
28
  "start": "node dist/server.js",
21
- "start:stdio": "TUTELIQ_MCP_TRANSPORT=stdio node dist/index.js",
22
- "prepublishOnly": "npm run build"
29
+ "start:stdio": "TUTELIQ_MCP_TRANSPORT=stdio node dist/src/index.js",
30
+ "prepublishOnly": "npm run clean && npm run build"
23
31
  },
24
32
  "keywords": [
25
33
  "tuteliq",
package/dist/index.d.ts DELETED
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
3
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}