multi-app-mcp 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.
@@ -0,0 +1,280 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // TEAMS — Team management tools
3
+ // ─────────────────────────────────────────────────────────────────────────────
4
+
5
+ import { getAuthHeaders, requireLogin, session } from "./session.js";
6
+
7
+ const QR_API_URL = process.env.QR_API_URL || "https://app.quickreviewer.com/api";
8
+
9
+ // ─── Tool Definitions ─────────────────────────────────────────────────────────
10
+
11
+ export const tools = [
12
+ {
13
+ name: "qr_get_teams",
14
+ description:
15
+ "Get list of all teams with pagination and optional search. " +
16
+ "sort: field to sort by (use 'name'). order: 'asc' or 'desc'. page: page number (start from 1). " +
17
+ "To get ALL teams, keep fetching next pages until no more results. " +
18
+ "User must be logged in.",
19
+ inputSchema: {
20
+ type: "object",
21
+ properties: {
22
+ sort: { type: "string", description: "Sort field — use 'name'" },
23
+ order: { type: "string", description: "'asc' or 'desc'" },
24
+ page: { type: "integer", description: "Page number (start from 1)" },
25
+ search: { type: "string", description: "Optional search keyword to filter teams" },
26
+ },
27
+ required: ["sort", "order", "page"],
28
+ },
29
+ },
30
+ {
31
+ name: "qr_add_team_member",
32
+ description:
33
+ "Add a new member to an existing team. " +
34
+ "ALWAYS ask the user for ALL of these before calling this tool: " +
35
+ "1) teamId (fetch from qr_get_teams if not known), " +
36
+ "2) member full name, " +
37
+ "3) member email address, " +
38
+ "4) role — must be one of: 'Reviewer', 'Team member', 'Admin', " +
39
+ "5) password for the new member account. " +
40
+ "Do NOT call this tool until all 5 values are provided. " +
41
+ "User must be logged in.",
42
+ inputSchema: {
43
+ type: "object",
44
+ properties: {
45
+ teamId: { type: "integer", description: "Team ID to add member to" },
46
+ name: { type: "string", description: "Display name of the member" },
47
+ email: { type: "string", description: "Email address of the member" },
48
+ role: { type: "string", description: "Role: 'Reviewer', 'Team member', or 'Admin'" },
49
+ password: { type: "string", description: "Password for the new member account" },
50
+ sendEmail: { type: "integer", description: "Send invitation email: 1 = yes, 0 = no (default 1)" },
51
+ },
52
+ required: ["teamId", "name", "email", "role", "password"],
53
+ },
54
+ },
55
+ {
56
+ name: "qr_delete_team",
57
+ description: "Delete a team by teamId. User must be logged in.",
58
+ inputSchema: {
59
+ type: "object",
60
+ properties: {
61
+ teamId: { type: "integer", description: "Team ID to delete" },
62
+ },
63
+ required: ["teamId"],
64
+ },
65
+ },
66
+ {
67
+ name: "qr_get_team_members",
68
+ description:
69
+ "Get members list with pagination using /team/get-team-user2. " +
70
+ "page starts from 1, each page returns 10 members. " +
71
+ "To get ALL members keep fetching next pages until no more results. " +
72
+ "Use teamId=0 to get all members across all teams. " +
73
+ "User must be logged in.",
74
+ inputSchema: {
75
+ type: "object",
76
+ properties: {
77
+ teamId: { type: "integer", description: "Team ID to filter members (0 = all teams)" },
78
+ page: { type: "integer", description: "Page number (start from 1)" },
79
+ sort: { type: "string", description: "Sort field — use 'name'" },
80
+ order: { type: "string", description: "'asc' or 'desc'" },
81
+ search: { type: "string", description: "Optional search keyword" },
82
+ role: { type: "string", description: "Optional role filter: 'Reviewer', 'Team member', 'Admin'" },
83
+ },
84
+ required: ["teamId", "page"],
85
+ },
86
+ },
87
+ {
88
+ name: "qr_get_all_team_members",
89
+ description:
90
+ "Get all team members for a specific document or folder. " +
91
+ "User must be logged in.",
92
+ inputSchema: {
93
+ type: "object",
94
+ properties: {
95
+ folderId: { type: "integer", description: "Folder ID (use 0 if not applicable)" },
96
+ documentId: { type: "integer", description: "Document ID (use 0 if not applicable)" },
97
+ },
98
+ required: ["folderId", "documentId"],
99
+ },
100
+ },
101
+ ];
102
+
103
+ // ─── Handlers ─────────────────────────────────────────────────────────────────
104
+
105
+ export const handlers = {
106
+
107
+ qr_get_teams: async ({ sort, order, page }) => {
108
+ const authError = requireLogin();
109
+ if (authError) return authError;
110
+
111
+ const res = await fetch(`${QR_API_URL}/team-list`, {
112
+ method: "POST",
113
+ headers: getAuthHeaders(),
114
+ body: JSON.stringify({ sort, order, page }),
115
+ });
116
+ const data = await res.json();
117
+
118
+ if (res.ok) {
119
+ const total = data.total_count || 0;
120
+ const items = data.items || [];
121
+ return {
122
+ content: [{
123
+ type: "text",
124
+ text:
125
+ `✅ Teams fetched — Page ${page} (${items.length} of ${total} total)\n\n` +
126
+ `${JSON.stringify(data, null, 2)}`,
127
+ }],
128
+ };
129
+ }
130
+ return {
131
+ content: [{
132
+ type: "text",
133
+ text: `❌ Failed (HTTP ${res.status})\n\n${JSON.stringify(data, null, 2)}`,
134
+ }],
135
+ isError: true,
136
+ };
137
+ },
138
+
139
+ qr_add_team_member: async ({ teamId, name, email, role, password, sendEmail = 1 }) => {
140
+ const authError = requireLogin();
141
+ if (authError) return authError;
142
+
143
+ // Step 1: Check if user already exists
144
+ const checkRes = await fetch(`${QR_API_URL}/team/is-user-exist`, {
145
+ method: "POST",
146
+ headers: getAuthHeaders(),
147
+ body: JSON.stringify({ email }),
148
+ });
149
+ const checkData = await checkRes.json();
150
+
151
+ const userExists = checkData.isExist || checkData.exist || checkData.data?.isExist || false;
152
+
153
+ // Step 2: Add member
154
+ // teamId null bhejo taake backend insert mode mein jaye (update nahi)
155
+ // userExists=true → password nahi chahiye (existing account)
156
+ // userExists=false → password chahiye (naya account)
157
+ const body = userExists
158
+ ? { teamId: null, name, email, role, sendEmail }
159
+ : { teamId: null, name, email, role, password, sendEmail };
160
+
161
+ const res = await fetch(`${QR_API_URL}/team/team-add-edit2`, {
162
+ method: "POST",
163
+ headers: getAuthHeaders(),
164
+ body: JSON.stringify(body),
165
+ });
166
+ const data = await res.json();
167
+
168
+ if (res.ok) {
169
+ return {
170
+ content: [{
171
+ type: "text",
172
+ text:
173
+ `✅ Team member added successfully!\n\n` +
174
+ `Name: ${name}\n` +
175
+ `Email: ${email}\n` +
176
+ `Role: ${role}\n` +
177
+ `Team ID: ${teamId}\n` +
178
+ `User was: ${userExists ? "existing account" : "new account created"}\n\n` +
179
+ `${JSON.stringify(data, null, 2)}`,
180
+ }],
181
+ };
182
+ }
183
+ return {
184
+ content: [{
185
+ type: "text",
186
+ text: `❌ Failed (HTTP ${res.status})\n\n${JSON.stringify(data, null, 2)}`,
187
+ }],
188
+ isError: true,
189
+ };
190
+ },
191
+
192
+ qr_delete_team: async ({ teamId }) => {
193
+ const authError = requireLogin();
194
+ if (authError) return authError;
195
+
196
+ const res = await fetch(`${QR_API_URL}/team-delete`, {
197
+ method: "POST",
198
+ headers: getAuthHeaders(),
199
+ body: JSON.stringify({ teamId }),
200
+ });
201
+ const data = await res.json();
202
+
203
+ if (res.ok) {
204
+ return {
205
+ content: [{
206
+ type: "text",
207
+ text: `✅ Team deleted successfully!\n\nTeam ID: ${teamId}`,
208
+ }],
209
+ };
210
+ }
211
+ return {
212
+ content: [{
213
+ type: "text",
214
+ text: `❌ Failed (HTTP ${res.status})\n\n${JSON.stringify(data, null, 2)}`,
215
+ }],
216
+ isError: true,
217
+ };
218
+ },
219
+
220
+ qr_get_team_members: async ({ teamId, page = 1, sort = "name", order = "asc", search = "", role = "" }) => {
221
+ const authError = requireLogin();
222
+ if (authError) return authError;
223
+
224
+ const res = await fetch(`${QR_API_URL}/team/get-team-user2`, {
225
+ method: "POST",
226
+ headers: getAuthHeaders(),
227
+ body: JSON.stringify({ teamId, page, sort, order, search, role }),
228
+ });
229
+ const data = await res.json();
230
+
231
+ if (res.ok) {
232
+ const total = data.count || data.total || 0;
233
+ const rows = data.rows || [];
234
+ return {
235
+ content: [{
236
+ type: "text",
237
+ text:
238
+ `✅ Team members fetched — Page ${page} (${rows.length} of ${total} total)\n\n` +
239
+ `${JSON.stringify(data, null, 2)}`,
240
+ }],
241
+ };
242
+ }
243
+ return {
244
+ content: [{
245
+ type: "text",
246
+ text: `❌ Failed (HTTP ${res.status})\n\n${JSON.stringify(data, null, 2)}`,
247
+ }],
248
+ isError: true,
249
+ };
250
+ },
251
+
252
+ qr_get_all_team_members: async ({ folderId, documentId }) => {
253
+ const authError = requireLogin();
254
+ if (authError) return authError;
255
+
256
+ const userId = session.userId;
257
+ const res = await fetch(`${QR_API_URL}/allteam/${userId}/${folderId}/${documentId}`, {
258
+ method: "GET",
259
+ headers: getAuthHeaders(),
260
+ });
261
+ const data = await res.json();
262
+
263
+ if (res.ok) {
264
+ return {
265
+ content: [{
266
+ type: "text",
267
+ text: `✅ All team members fetched\n\n${JSON.stringify(data, null, 2)}`,
268
+ }],
269
+ };
270
+ }
271
+ return {
272
+ content: [{
273
+ type: "text",
274
+ text: `❌ Failed (HTTP ${res.status})\n\n${JSON.stringify(data, null, 2)}`,
275
+ }],
276
+ isError: true,
277
+ };
278
+ },
279
+
280
+ };
@@ -0,0 +1,234 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // UPLOAD — File upload tools
3
+ // 2 approaches:
4
+ // 1. qr_upload_document → user local file path deta hai
5
+ // 2. qr_upload_from_content → user + button se file attach karta hai Claude mein
6
+ //
7
+ // 2-step process:
8
+ // Step 1: /file-upload/upload.php → multipart file upload (no size limit)
9
+ // Step 2: /insert-new-document → DB metadata + thumbnail trigger
10
+ //
11
+ // Filename format: {fileSize}-{sanitizedName}{ext}
12
+ // Matches browser's resumable.js uniqueIdentifier + extension pattern
13
+ // ─────────────────────────────────────────────────────────────────────────────
14
+
15
+ import { getAuthHeaders, requireLogin, session } from "./session.js";
16
+ import fs from "fs";
17
+ import path from "path";
18
+
19
+ const QR_API_URL = process.env.QR_API_URL || "https://app.quickreviewer.com/api";
20
+ const QR_UPLOAD_URL = process.env.QR_UPLOAD_URL || "https://app.quickreviewer.com/file-upload";
21
+
22
+ // ─── Tool Definitions ─────────────────────────────────────────────────────────
23
+
24
+ export const tools = [
25
+ {
26
+ name: "qr_upload_document",
27
+ description:
28
+ "Upload a local file to Quickreviewer by providing the full file path. " +
29
+ "ALWAYS use this tool directly — do NOT open browser, do NOT open any URL. " +
30
+ "This tool reads the file from local filesystem via Node.js and uploads directly to QR API. " +
31
+ "Ask user for full local file path if not provided (e.g. C:\\Users\\azhar\\Downloads\\file.png). " +
32
+ "folderId: folder to upload into (0 = root). " +
33
+ "User must be logged in.",
34
+ inputSchema: {
35
+ type: "object",
36
+ properties: {
37
+ filePath: { type: "string", description: "Full local path to the file e.g. C:\\Users\\azhar\\Downloads\\image.png" },
38
+ folderId: { type: "integer", description: "Folder ID to upload into (0 = root)" },
39
+ documentId: { type: "integer", description: "Leave empty for new document, or provide existing documentId to add new version" },
40
+ },
41
+ required: ["filePath", "folderId"],
42
+ },
43
+ },
44
+ {
45
+ name: "qr_upload_from_content",
46
+ description:
47
+ "Upload a file to Quickreviewer when user has attached a file directly in Claude chat using + button. " +
48
+ "ALWAYS use this tool when a file is attached in the conversation — do NOT ask for file path. " +
49
+ "Do NOT open browser, do NOT ask where file is saved — just read the attached file content and call this tool. " +
50
+ "fileContent: base64 encoded content of the attached file. " +
51
+ "fileName: original filename from the attachment. " +
52
+ "folderId: folder to upload into (0 = root) — ask user if not specified. " +
53
+ "User must be logged in.",
54
+ inputSchema: {
55
+ type: "object",
56
+ properties: {
57
+ fileContent: { type: "string", description: "Base64 encoded content of the file attached by user" },
58
+ fileName: { type: "string", description: "Original filename with extension e.g. image.png" },
59
+ folderId: { type: "integer", description: "Folder ID to upload into (0 = root)" },
60
+ documentId: { type: "integer", description: "Leave empty for new document, or existing documentId for new version" },
61
+ },
62
+ required: ["fileContent", "fileName", "folderId"],
63
+ },
64
+ },
65
+ ];
66
+
67
+ // ─── Handlers ─────────────────────────────────────────────────────────────────
68
+
69
+ export const handlers = {
70
+
71
+ // ── Approach 1: Local file path ──────────────────────────────────────────────
72
+ qr_upload_document: async ({ filePath, folderId, documentId = null }) => {
73
+ const authError = requireLogin();
74
+ if (authError) return authError;
75
+
76
+ if (!fs.existsSync(filePath)) {
77
+ return {
78
+ content: [{
79
+ type: "text",
80
+ text: `❌ File not found: ${filePath}\n\nPlease provide a valid full file path.`,
81
+ }],
82
+ isError: true,
83
+ };
84
+ }
85
+
86
+ const fileBuffer = fs.readFileSync(filePath);
87
+ const originalname = path.basename(filePath);
88
+
89
+ return await uploadToQR(fileBuffer, originalname, folderId, documentId);
90
+ },
91
+
92
+ // ── Approach 2: File content from Claude attachment ──────────────────────────
93
+ qr_upload_from_content: async ({ fileContent, fileName, folderId, documentId = null }) => {
94
+ const authError = requireLogin();
95
+ if (authError) return authError;
96
+
97
+ // base64 content ko buffer mein convert karo
98
+ let fileBuffer;
99
+ try {
100
+ fileBuffer = Buffer.from(fileContent, "base64");
101
+ } catch {
102
+ fileBuffer = Buffer.from(fileContent, "utf-8");
103
+ }
104
+
105
+ return await uploadToQR(fileBuffer, fileName, folderId, documentId);
106
+ },
107
+
108
+ };
109
+
110
+ // ─── Shared Upload Logic ──────────────────────────────────────────────────────
111
+
112
+ async function uploadToQR(fileBuffer, originalname, folderId, documentId) {
113
+ const fileSize = fileBuffer.length;
114
+ const mimeType = getMimeType(originalname);
115
+ const ext = originalname.includes('.') ? '.' + originalname.split('.').pop() : '';
116
+ // Match browser resumable.js pattern: {size}-{sanitizedName}{ext}
117
+ // Sanitize = strip extension, remove non-alphanumeric chars (same as resumable.js default)
118
+ const sanitized = originalname.replace(/\.[^.]+$/, '').replace(/[^0-9a-zA-Z_-]/g, '');
119
+ const identifier = `${fileSize}-${sanitized}`;
120
+ const filename = `${identifier}${ext}`;
121
+
122
+ // Step 1: Upload file via multipart (no size limit, matches browser flow)
123
+ const uploadParams = new URLSearchParams({
124
+ resumableChunkNumber: "1",
125
+ resumableChunkSize: "20971520",
126
+ resumableCurrentChunkSize: String(fileSize),
127
+ resumableTotalSize: String(fileSize),
128
+ resumableType: mimeType,
129
+ resumableIdentifier: identifier,
130
+ resumableFilename: filename,
131
+ resumableRelativePath: originalname,
132
+ resumableTotalChunks: "1",
133
+ });
134
+
135
+ const formData = new FormData();
136
+ const blob = new Blob([fileBuffer], { type: mimeType });
137
+ formData.append("file", blob, filename);
138
+
139
+ const authHeaders = getAuthHeaders();
140
+ delete authHeaders["Content-Type"]; // FormData sets its own Content-Type
141
+
142
+ const uploadRes = await fetch(`${QR_UPLOAD_URL}/upload.php?${uploadParams.toString()}`, {
143
+ method: "POST",
144
+ headers: authHeaders,
145
+ body: formData,
146
+ });
147
+
148
+ if (!uploadRes.ok) {
149
+ const uploadText = await uploadRes.text().catch(() => "");
150
+ return {
151
+ content: [{
152
+ type: "text",
153
+ text: `❌ File upload to server failed (HTTP ${uploadRes.status})\n\n${uploadText.slice(0, 500)}`,
154
+ }],
155
+ isError: true,
156
+ };
157
+ }
158
+
159
+ // Step 2: Save metadata to DB + trigger thumbnail processing
160
+ const insertBody = {
161
+ user_id: session.userId,
162
+ filename: filename,
163
+ originalname: originalname,
164
+ size: fileSize,
165
+ mimetype: mimeType,
166
+ companyName: "",
167
+ contentType: "",
168
+ versionNo: 1,
169
+ documentId: documentId || null,
170
+ folderId: folderId,
171
+ workspaceId: 0,
172
+ ownerId: session.userId,
173
+ priv: true,
174
+ };
175
+
176
+ const insertRes = await fetch(`${QR_API_URL}/insert-new-document`, {
177
+ method: "POST",
178
+ headers: getAuthHeaders(),
179
+ body: JSON.stringify(insertBody),
180
+ });
181
+ const insertData = await insertRes.json().catch(() => ({}));
182
+
183
+ if (insertRes.ok) {
184
+ return {
185
+ content: [{
186
+ type: "text",
187
+ text:
188
+ `✅ File uploaded successfully!\n\n` +
189
+ `File: ${originalname}\n` +
190
+ `Size: ${(fileSize / 1024).toFixed(1)} KB\n` +
191
+ `Folder: ${folderId}\n` +
192
+ `Type: ${mimeType}\n\n` +
193
+ `${JSON.stringify(insertData, null, 2)}`,
194
+ }],
195
+ };
196
+ }
197
+
198
+ return {
199
+ content: [{
200
+ type: "text",
201
+ text:
202
+ `⚠️ File uploaded to server but DB save failed (HTTP ${insertRes.status})\n\n` +
203
+ `${JSON.stringify(insertData, null, 2)}`,
204
+ }],
205
+ isError: true,
206
+ };
207
+ }
208
+
209
+ // ─── Helper ───────────────────────────────────────────────────────────────────
210
+
211
+ function getMimeType(filename) {
212
+ const ext = filename.split('.').pop().toLowerCase();
213
+ const map = {
214
+ png: "image/png",
215
+ jpg: "image/jpeg",
216
+ jpeg: "image/jpeg",
217
+ gif: "image/gif",
218
+ bmp: "image/bmp",
219
+ pdf: "application/pdf",
220
+ mp4: "video/mp4",
221
+ webm: "video/webm",
222
+ avi: "video/avi",
223
+ mp3: "audio/mp3",
224
+ zip: "application/zip",
225
+ doc: "application/msword",
226
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
227
+ xls: "application/vnd.ms-excel",
228
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
229
+ ppt: "application/vnd.ms-powerpoint",
230
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
231
+ txt: "text/plain",
232
+ };
233
+ return map[ext] || "application/octet-stream";
234
+ }
@@ -0,0 +1,14 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // WORKFLOWS — Workflow & review tools
3
+ // ─────────────────────────────────────────────────────────────────────────────
4
+
5
+ // import { getAuthHeaders, requireLogin } from "./session.js";
6
+ // const QR_API_URL = process.env.QR_API_URL || "https://beta.quickreviewer.com/api";
7
+
8
+ export const tools = [
9
+ // Tier 4 tools yahan aayenge
10
+ ];
11
+
12
+ export const handlers = {
13
+ // Tier 4 handlers yahan aayenge
14
+ };