@cobo-screenshot/mcp 0.1.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.
Files changed (2) hide show
  1. package/dist/index.js +164 -0
  2. package/package.json +32 -0
package/dist/index.js ADDED
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { z } from "zod";
7
+
8
+ // ../../packages/core/dist/index.js
9
+ var DEFAULT_COBO_API_URL = "https://api.cobo.example";
10
+
11
+ // src/client.ts
12
+ var CoboClient = class {
13
+ apiUrl;
14
+ token;
15
+ fetchImpl;
16
+ constructor(opts) {
17
+ this.apiUrl = opts.apiUrl.replace(/\/$/, "");
18
+ this.token = opts.token;
19
+ this.fetchImpl = opts.fetchImpl ?? fetch;
20
+ }
21
+ async req(path, init = {}) {
22
+ const res = await this.fetchImpl(`${this.apiUrl}${path}`, {
23
+ ...init,
24
+ headers: {
25
+ ...init.body ? { "content-type": "application/json" } : {},
26
+ Authorization: `Bearer ${this.token}`,
27
+ ...init.headers ?? {}
28
+ }
29
+ });
30
+ if (!res.ok) {
31
+ let message = `${init.method ?? "GET"} ${path} -> ${res.status}`;
32
+ try {
33
+ const body = await res.json();
34
+ if (body?.error) message = body.error;
35
+ } catch {
36
+ }
37
+ throw new Error(message);
38
+ }
39
+ return await res.json();
40
+ }
41
+ getThread(id) {
42
+ return this.req(`/threads/${id}`);
43
+ }
44
+ listThreads(params) {
45
+ const q = new URLSearchParams({ workspaceId: params.workspaceId });
46
+ if (params.url) q.set("url", params.url);
47
+ return this.req(`/threads?${q.toString()}`);
48
+ }
49
+ addComment(id, body) {
50
+ return this.req(`/threads/${id}/comments`, {
51
+ method: "POST",
52
+ body: JSON.stringify({ body })
53
+ });
54
+ }
55
+ setStatus(id, status) {
56
+ return this.req(`/threads/${id}`, {
57
+ method: "PATCH",
58
+ body: JSON.stringify({ status })
59
+ });
60
+ }
61
+ };
62
+
63
+ // src/format.ts
64
+ var AI_PREFIX = "\u{1F916} (via AI) ";
65
+ function withAiPrefix(body) {
66
+ return AI_PREFIX + body;
67
+ }
68
+ function formatComment(t) {
69
+ const a = t.anchor;
70
+ return {
71
+ nodeId: t.id,
72
+ url: t.url,
73
+ status: t.status,
74
+ version: t.version,
75
+ element: a.type === "element" ? { cssPath: a.cssPath, tag: a.tag, attrs: a.attrs, text: a.text } : { kind: "region", rect: a.rect },
76
+ screenshot: t.imageUrl ? { url: t.imageUrl } : null,
77
+ comments: t.comments.map((c) => ({ author: c.author.name, body: c.body, createdAt: c.createdAt })),
78
+ createdAt: t.createdAt
79
+ };
80
+ }
81
+ function formatList(threads) {
82
+ return threads.map((t) => ({
83
+ nodeId: t.id,
84
+ url: t.url,
85
+ status: t.status,
86
+ snippet: (t.comments[0]?.body ?? "").slice(0, 140),
87
+ element: t.anchor.type === "element" ? { tag: t.anchor.tag, text: t.anchor.text } : { tag: "(region)", text: "" },
88
+ author: t.comments[0]?.author.name ?? "",
89
+ createdAt: t.createdAt
90
+ }));
91
+ }
92
+
93
+ // src/index.ts
94
+ var apiUrl = process.env.COBO_API_URL ?? DEFAULT_COBO_API_URL;
95
+ var token = process.env.COBO_TOKEN;
96
+ if (!token) {
97
+ console.error("cobo-mcp: COBO_TOKEN must be set. Generate one in the Cobo extension's AI access panel.");
98
+ process.exit(1);
99
+ }
100
+ var client = new CoboClient({ apiUrl, token });
101
+ var server = new McpServer({ name: "cobo", version: "0.0.0" });
102
+ async function fetchImageBlock(url) {
103
+ try {
104
+ const res = await fetch(url);
105
+ if (!res.ok) return null;
106
+ const mimeType = (res.headers.get("content-type") ?? "image/jpeg").split(";")[0].trim();
107
+ const data = Buffer.from(await res.arrayBuffer()).toString("base64");
108
+ return { type: "image", data, mimeType };
109
+ } catch {
110
+ return null;
111
+ }
112
+ }
113
+ server.tool(
114
+ "get_comment",
115
+ "Fetch a Cobo comment thread by its nodeId: the comments, the page element it points to (cssPath/tag/text), a screenshot, the page URL, and status.",
116
+ { nodeId: z.string().describe("The comment's nodeId (the Cobo thread id)") },
117
+ async ({ nodeId }) => {
118
+ const { thread } = await client.getThread(nodeId);
119
+ const ctx = formatComment(thread);
120
+ const content = [
121
+ { type: "text", text: JSON.stringify(ctx, null, 2) }
122
+ ];
123
+ if (ctx.screenshot) {
124
+ const img = await fetchImageBlock(ctx.screenshot.url);
125
+ if (img) content.push(img);
126
+ }
127
+ return { content };
128
+ }
129
+ );
130
+ server.tool(
131
+ "list_comments",
132
+ "List Cobo comment threads for a workspace, optionally filtered by page url and status.",
133
+ {
134
+ workspaceId: z.string().describe("The Cobo workspace id (included in the comment handoff prompt)"),
135
+ url: z.string().optional(),
136
+ status: z.enum(["open", "resolved"]).optional()
137
+ },
138
+ async ({ workspaceId, url, status }) => {
139
+ const { threads } = await client.listThreads({ workspaceId, url });
140
+ let list = formatList(threads);
141
+ if (status) list = list.filter((t) => t.status === status);
142
+ return { content: [{ type: "text", text: JSON.stringify(list, null, 2) }] };
143
+ }
144
+ );
145
+ server.tool(
146
+ "reply_comment",
147
+ "Post a reply to a Cobo comment thread. The reply is marked as written via AI.",
148
+ { nodeId: z.string(), body: z.string() },
149
+ async ({ nodeId, body }) => {
150
+ const { comment } = await client.addComment(nodeId, withAiPrefix(body));
151
+ return { content: [{ type: "text", text: `Replied to ${nodeId} (commentId=${comment.id}).` }] };
152
+ }
153
+ );
154
+ server.tool(
155
+ "resolve_comment",
156
+ "Mark a Cobo comment thread resolved, or reopen it.",
157
+ { nodeId: z.string(), reopen: z.boolean().optional() },
158
+ async ({ nodeId, reopen }) => {
159
+ const { thread } = await client.setStatus(nodeId, reopen ? "open" : "resolved");
160
+ return { content: [{ type: "text", text: `Thread ${thread.id} is now ${thread.status}.` }] };
161
+ }
162
+ );
163
+ var transport = new StdioServerTransport();
164
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@cobo-screenshot/mcp",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "cobo-mcp": "./dist/index.js"
7
+ },
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "dependencies": {
15
+ "@modelcontextprotocol/sdk": "^1.12.0",
16
+ "zod": "^3.23.8"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^22.10.2",
20
+ "tsup": "^8.3.0",
21
+ "tsx": "^4.19.2",
22
+ "typescript": "^5.6.0",
23
+ "vitest": "^2.1.4",
24
+ "@cobo/core": "0.0.0"
25
+ },
26
+ "scripts": {
27
+ "build": "tsup",
28
+ "dev": "tsx src/index.ts",
29
+ "test": "vitest run",
30
+ "typecheck": "tsc --noEmit"
31
+ }
32
+ }