viewgate-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.
package/dist/index.js ADDED
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env node
2
+ import express from "express";
3
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
6
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
7
+ import fetch from "node-fetch";
8
+ import cors from "cors";
9
+ import dotenv from "dotenv";
10
+ dotenv.config();
11
+ const port = process.env.PORT || 3000;
12
+ const BACKEND_URL = process.env.BACKEND_URL || "https://view-gate.vercel.app";
13
+ // Store active sessions for SSE: sessionId -> { server, transport }
14
+ const sessions = new Map();
15
+ /**
16
+ * Create a new MCP server instance for a specific context.
17
+ */
18
+ function createMcpServer(apiKey) {
19
+ const server = new Server({
20
+ name: "viewgate-mcp",
21
+ version: "1.0.0",
22
+ }, {
23
+ capabilities: {
24
+ tools: {},
25
+ },
26
+ });
27
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
28
+ return {
29
+ tools: [
30
+ {
31
+ name: "get_annotations",
32
+ description: "Retrieves all feedback annotations. For each annotation, follow the '_ia_fix_instruction' to perform a FAST, SURGICAL fix. Use 'outerHtml' and 'source' (file:line) to locate the exact code block without manual searching.",
33
+ inputSchema: {
34
+ type: "object",
35
+ properties: {},
36
+ },
37
+ },
38
+ {
39
+ name: "mark_annotation_ready",
40
+ description: "Mark an annotation as ready for review after applying changes, and generate a local Markdown log.",
41
+ inputSchema: {
42
+ type: "object",
43
+ properties: {
44
+ id: {
45
+ type: "string",
46
+ description: "The ID of the annotation to mark as ready"
47
+ },
48
+ originalMessage: {
49
+ type: "string",
50
+ description: "The original feedback message for the changelog"
51
+ },
52
+ appliedChanges: {
53
+ type: "string",
54
+ description: "Brief summary of what was changed in the code"
55
+ }
56
+ },
57
+ required: ["id", "originalMessage", "appliedChanges"]
58
+ },
59
+ }
60
+ ],
61
+ };
62
+ });
63
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
64
+ switch (request.params.name) {
65
+ case "get_annotations": {
66
+ try {
67
+ const statuses = 'pending,bug_fixing';
68
+ const response = await fetch(`${BACKEND_URL}/api/mcp/annotations?status=${statuses}`, {
69
+ headers: {
70
+ 'x-api-key': apiKey
71
+ }
72
+ });
73
+ if (!response.ok) {
74
+ throw new Error(`Backend responded with ${response.status}`);
75
+ }
76
+ const data = (await response.json());
77
+ const rawAnnotations = Array.isArray(data) ? data : (data?.data || []);
78
+ if (!Array.isArray(rawAnnotations)) {
79
+ return {
80
+ content: [{ type: "text", text: "Error: Invalid format" }],
81
+ isError: true
82
+ };
83
+ }
84
+ const priorityMap = {
85
+ 'urgente': 4,
86
+ 'urgent': 4,
87
+ 'alta': 3,
88
+ 'high': 3,
89
+ 'media': 2,
90
+ 'medium': 2,
91
+ 'baja': 1,
92
+ 'low': 1
93
+ };
94
+ const sortedAnnotations = rawAnnotations.sort((a, b) => {
95
+ const statusOrder = {
96
+ 'pending': 2,
97
+ 'bug_fixing': 1
98
+ };
99
+ const statusA = (a.status || 'pending').toLowerCase();
100
+ const statusB = (b.status || 'pending').toLowerCase();
101
+ if (statusOrder[statusA] !== statusOrder[statusB]) {
102
+ return (statusOrder[statusB] || 0) - (statusOrder[statusA] || 0);
103
+ }
104
+ const prioA = priorityMap[(a.priority || 'medium').toLowerCase()] || 0;
105
+ const prioB = priorityMap[(b.priority || 'medium').toLowerCase()] || 0;
106
+ return prioB - prioA;
107
+ });
108
+ const annotationsWithTips = sortedAnnotations.map((ann) => {
109
+ const source = ann.reference?.source;
110
+ const hasSource = source && source !== 'unknown:0';
111
+ const [file, line] = hasSource ? source.split(':') : [null, null];
112
+ // Essential fields for the AI to perform a surgical fix
113
+ return {
114
+ id: ann._id,
115
+ key: ann.key,
116
+ priority: ann.priority,
117
+ status: ann.status,
118
+ message: ann.message,
119
+ source: source,
120
+ filePath: ann.filePath || file,
121
+ line: ann.line || (line ? parseInt(line) : undefined),
122
+ outerHtml: ann.reference?.outerHtml,
123
+ parentContext: ann.reference?.parentContext,
124
+ componentPath: ann.reference?.componentPath,
125
+ selector: ann.reference?.selector,
126
+ tag: ann.reference?.tag,
127
+ _ia_fix_instruction: hasSource
128
+ ? `FAST SURGICAL FIX: Open \`${file}\` at line ${line}. Focus on the block inside \`${ann.reference?.parentContext?.slice(0, 50)}...\`. Exact element: \`${ann.reference?.outerHtml?.slice(0, 100)}...\`. Instruction: ${ann.message}`
129
+ : `MANUAL FIND: Search for the element using selector \`${ann.reference?.selector}\`. Reference HTML: \`${ann.reference?.outerHtml?.slice(0, 100)}...\`. Instruction: ${ann.message}`
130
+ };
131
+ });
132
+ return {
133
+ content: [
134
+ {
135
+ type: "text",
136
+ text: JSON.stringify(annotationsWithTips, null, 2),
137
+ },
138
+ ],
139
+ };
140
+ }
141
+ catch (error) {
142
+ return {
143
+ content: [
144
+ {
145
+ type: "text",
146
+ text: `Error fetching annotations: ${error.message}`,
147
+ },
148
+ ],
149
+ isError: true,
150
+ };
151
+ }
152
+ }
153
+ case "mark_annotation_ready": {
154
+ try {
155
+ const { id, appliedChanges } = request.params.arguments;
156
+ const response = await fetch(`${BACKEND_URL}/api/annotations/${id}`, {
157
+ method: 'PATCH',
158
+ headers: {
159
+ 'Content-Type': 'application/json',
160
+ 'x-api-key': apiKey
161
+ },
162
+ body: JSON.stringify({ status: 'ready_for_review' })
163
+ });
164
+ if (!response.ok) {
165
+ throw new Error(`Backend responded with ${response.status} when updating annotation`);
166
+ }
167
+ return {
168
+ content: [{ type: "text", text: `Successfully marked annotation ${id} as ready_for_review.` }],
169
+ };
170
+ }
171
+ catch (error) {
172
+ return {
173
+ content: [{ type: "text", text: `Error marking annotation ready: ${error.message}` }],
174
+ isError: true,
175
+ };
176
+ }
177
+ }
178
+ default:
179
+ throw new Error("Unknown tool");
180
+ }
181
+ });
182
+ return server;
183
+ }
184
+ // --- TRANSPORT SELECTION ---
185
+ const useSSE = process.argv.includes("--sse") || process.env.MCP_TRANSPORT === "sse";
186
+ if (!useSSE) {
187
+ // STDIO Mode (NPM Package Default)
188
+ // In Stdio mode, we use the API key from environment variable
189
+ const apiKey = process.env.VIEWGATE_API_KEY || process.env.API_KEY || "";
190
+ if (!apiKey) {
191
+ console.error("Error: VIEWGATE_API_KEY environment variable is required in STDIO mode.");
192
+ process.exit(1);
193
+ }
194
+ const server = createMcpServer(apiKey);
195
+ const transport = new StdioServerTransport();
196
+ server.connect(transport).catch((error) => {
197
+ console.error("Failed to start Stdio transport:", error);
198
+ process.exit(1);
199
+ });
200
+ }
201
+ else {
202
+ // SSE Mode (Express/Remote)
203
+ const app = express();
204
+ const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS ? process.env.ALLOWED_ORIGINS.split(',') : '*';
205
+ app.use(cors({
206
+ origin: ALLOWED_ORIGINS,
207
+ methods: ['GET', 'POST', 'OPTIONS'],
208
+ allowedHeaders: ['Content-Type', 'x-api-key', 'Authorization']
209
+ }));
210
+ // Apply JSON middleware only for non-MCP routes
211
+ app.use((req, res, next) => {
212
+ if (req.path === "/message")
213
+ return next();
214
+ express.json()(req, res, next);
215
+ });
216
+ app.get("/sse", async (req, res) => {
217
+ const apiKey = req.query.apiKey || req.headers['x-api-key'];
218
+ if (!apiKey) {
219
+ res.status(401).send("API Key is required");
220
+ return;
221
+ }
222
+ const transport = new SSEServerTransport("/message", res);
223
+ const server = createMcpServer(apiKey);
224
+ await server.connect(transport);
225
+ const sessionId = transport.sessionId;
226
+ sessions.set(sessionId, { server, transport });
227
+ res.on("close", () => sessions.delete(sessionId));
228
+ });
229
+ app.post("/message", async (req, res) => {
230
+ const sessionId = req.query.sessionId;
231
+ const session = sessions.get(sessionId);
232
+ if (!session) {
233
+ res.status(404).send("Session not found");
234
+ return;
235
+ }
236
+ await session.transport.handlePostMessage(req, res);
237
+ });
238
+ app.listen(port, () => {
239
+ console.log(`ViewGate MCP (SSE) listening on port ${port}`);
240
+ });
241
+ }
@@ -0,0 +1,59 @@
1
+ import fetch from "node-fetch";
2
+ async function test() {
3
+ console.log("1. Connecting to http://localhost:3333/sse...");
4
+ const response = await fetch("http://localhost:3333/sse?apiKey=2af5628c9442f10b93cfe6970897d4de26578d958ba43a4e3bcb684fe0f92668");
5
+ if (!response.ok) {
6
+ console.error("Failed to connect to SSE:", response.status, await response.text());
7
+ return;
8
+ }
9
+ console.log("SSE connected. Reading stream for sessionId...");
10
+ const body = response.body;
11
+ let sessionId = null;
12
+ body.on("data", async (chunk) => {
13
+ const text = chunk.toString();
14
+ console.log("SSE Data:", text);
15
+ // Example event:
16
+ // event: endpoint
17
+ // data: /message?sessionId=...
18
+ const match = text.match(/sessionId=([a-f0-9-]+)/);
19
+ if (match && !sessionId) {
20
+ sessionId = match[1];
21
+ console.log("FOUND SESSION ID:", sessionId);
22
+ // 2. Test POST to /message
23
+ console.log(`2. Testing POST to /message?sessionId=${sessionId}...`);
24
+ const postResponse = await fetch(`http://localhost:3333/message?sessionId=${sessionId}`, {
25
+ method: 'POST',
26
+ headers: {
27
+ 'Content-Type': 'application/json'
28
+ },
29
+ body: JSON.stringify({
30
+ jsonrpc: "2.0",
31
+ id: 1,
32
+ method: "initialize",
33
+ params: {
34
+ clientInfo: { name: "test-client", version: "1.0.0" },
35
+ protocolVersion: "2024-11-05",
36
+ capabilities: {}
37
+ }
38
+ })
39
+ });
40
+ console.log("POST Response Status:", postResponse.status);
41
+ const responseText = await postResponse.text();
42
+ console.log("POST Response Body:", responseText);
43
+ if (postResponse.status === 200 || postResponse.status === 202) {
44
+ console.log("SUCCESS: Stream was readable and message was accepted.");
45
+ }
46
+ else {
47
+ console.error("FAILURE: Server returned error status.");
48
+ }
49
+ process.exit(0);
50
+ }
51
+ });
52
+ setTimeout(() => {
53
+ if (!sessionId) {
54
+ console.error("TIMEOUT: Did not receive sessionId from SSE.");
55
+ process.exit(1);
56
+ }
57
+ }, 10000);
58
+ }
59
+ test();
@@ -0,0 +1,23 @@
1
+ import fetch from "node-fetch";
2
+ async function test() {
3
+ console.log("Connecting to http://localhost:3333/sse...");
4
+ const response = await fetch("http://localhost:3333/sse?apiKey=test-key");
5
+ if (!response.ok) {
6
+ console.error("Failed to connect:", response.status, await response.text());
7
+ return;
8
+ }
9
+ console.log("Headers:", response.headers.raw());
10
+ const body = response.body;
11
+ body.on("data", (chunk) => {
12
+ console.log("Chunk received:", chunk.toString());
13
+ if (chunk.toString().includes("event: endpoint")) {
14
+ console.log("FOUND ENDPOINT EVENT!");
15
+ }
16
+ });
17
+ body.on("end", () => console.log("Connection closed"));
18
+ setTimeout(() => {
19
+ console.log("Closing test...");
20
+ process.exit(0);
21
+ }, 5000);
22
+ }
23
+ test();
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "viewgate-mcp",
3
+ "version": "1.0.0",
4
+ "main": "dist/index.js",
5
+ "bin": {
6
+ "viewgate-mcp": "./dist/index.js"
7
+ },
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "type": "module",
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "watch": "tsc -w",
15
+ "start": "node dist/index.js",
16
+ "dev": "tsx watch src/index.ts",
17
+ "test": "echo \"Error: no test specified\" && exit 1"
18
+ },
19
+ "keywords": [],
20
+ "author": "",
21
+ "license": "ISC",
22
+ "description": "",
23
+ "dependencies": {
24
+ "@modelcontextprotocol/sdk": "^1.27.1",
25
+ "cors": "^2.8.6",
26
+ "dotenv": "^17.3.1",
27
+ "express": "^5.2.1",
28
+ "node-fetch": "^3.3.2"
29
+ },
30
+ "devDependencies": {
31
+ "@types/cors": "^2.8.19",
32
+ "@types/express": "^5.0.6",
33
+ "@types/node": "^25.5.0",
34
+ "@types/node-fetch": "^2.6.13",
35
+ "nodemon": "^3.1.14",
36
+ "ts-node": "^10.9.2",
37
+ "tsx": "^4.21.0",
38
+ "typescript": "^5.9.3"
39
+ }
40
+ }