sencre-mcp 1.3.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 (3) hide show
  1. package/README.md +56 -0
  2. package/index.mjs +408 -0
  3. package/package.json +35 -0
package/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # sencre-mcp
2
+
3
+ MCP server for [Sencre](https://todos.sencre.tr) Kanban boards.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npx -y sencre-mcp
9
+ ```
10
+
11
+ Or after `npm i -g sencre-mcp`, run `sencre-mcp`.
12
+
13
+ ## Env
14
+
15
+ | Variable | Required | Description |
16
+ |----------|----------|-------------|
17
+ | `SENCRE_API_BASE` | no | Default `https://todos.sencre.tr` |
18
+ | `SENCRE_PROJECT_ID` | no | Override bound project |
19
+ | `SENCRE_AGENT_KEY` | no | Admin key (operators); users use pair login |
20
+
21
+ ## First use
22
+
23
+ 1. Add MCP to your client (see https://todos.sencre.tr/mcp).
24
+ 2. In chat: `/sencre` or call tool `login` (shown as `sencre_login`).
25
+ 3. Browser opens → log in → pick project.
26
+ 4. Session: `~/.sencre/session.json`.
27
+
28
+ ## Tools (short names)
29
+
30
+ `login`, `logout`, `auth_status`, `list_projects`, `get_board`, `list_tasks`, `create_task`, `update_task`, `move_task`, `complete_task`, `whats_left`
31
+
32
+ With server id `sencre`, clients typically display `sencre_whats_left`, etc.
33
+
34
+ ## Kilo example
35
+
36
+ ```json
37
+ {
38
+ "mcp": {
39
+ "sencre": {
40
+ "type": "local",
41
+ "command": ["npx", "-y", "sencre-mcp"],
42
+ "enabled": true,
43
+ "environment": {
44
+ "SENCRE_API_BASE": "https://todos.sencre.tr"
45
+ }
46
+ }
47
+ }
48
+ }
49
+ ```
50
+
51
+ From a git checkout (no npm publish):
52
+
53
+ ```json
54
+ "command": ["node", "path/to/mcp/sencre/index.mjs"],
55
+ "cwd": "path/to/mcp/sencre"
56
+ ```
package/index.mjs ADDED
@@ -0,0 +1,408 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Plain-Node MCP entry (no tsx). Windows / Kilo spawn safe.
4
+ */
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ import { z } from "zod";
8
+ import { exec } from "node:child_process";
9
+ import {
10
+ existsSync,
11
+ mkdirSync,
12
+ readFileSync,
13
+ writeFileSync,
14
+ unlinkSync,
15
+ } from "node:fs";
16
+ import { homedir } from "node:os";
17
+ import { dirname, join } from "node:path";
18
+
19
+ function logErr(...args) {
20
+ console.error("[sencre-mcp]", ...args);
21
+ }
22
+
23
+ // ——— session (~/.sencre/session.json) ———
24
+ function sessionDir() {
25
+ return join(homedir(), ".sencre");
26
+ }
27
+ function sessionFile() {
28
+ return join(sessionDir(), "session.json");
29
+ }
30
+ function loadSession() {
31
+ try {
32
+ const p = sessionFile();
33
+ if (!existsSync(p)) return null;
34
+ const s = JSON.parse(readFileSync(p, "utf8"));
35
+ if (!s.token || !s.projectId) return null;
36
+ if (s.expiresAt && new Date(s.expiresAt).getTime() < Date.now()) {
37
+ clearSession();
38
+ return null;
39
+ }
40
+ return s;
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+ function saveSession(s) {
46
+ const d = sessionDir();
47
+ if (!existsSync(d)) mkdirSync(d, { recursive: true });
48
+ writeFileSync(sessionFile(), `${JSON.stringify(s, null, 2)}\n`, "utf8");
49
+ }
50
+ function clearSession() {
51
+ try {
52
+ unlinkSync(sessionFile());
53
+ } catch {
54
+ /* ignore */
55
+ }
56
+ }
57
+
58
+ // ——— project bind ———
59
+ function resolveProjectIdFromCwd(cwd = process.cwd()) {
60
+ if (process.env.SENCRE_PROJECT_ID?.trim()) {
61
+ return process.env.SENCRE_PROJECT_ID.trim();
62
+ }
63
+ let dir = cwd;
64
+ for (let i = 0; i < 12; i++) {
65
+ const p = join(dir, ".sencre.json");
66
+ if (existsSync(p)) {
67
+ try {
68
+ const j = JSON.parse(readFileSync(p, "utf8"));
69
+ if (j.projectId) return String(j.projectId);
70
+ } catch {
71
+ /* ignore */
72
+ }
73
+ }
74
+ const parent = dirname(dir);
75
+ if (parent === dir) break;
76
+ dir = parent;
77
+ }
78
+ return undefined;
79
+ }
80
+
81
+ function apiBase() {
82
+ return (
83
+ process.env.SENCRE_API_BASE ||
84
+ loadSession()?.apiBase ||
85
+ "https://todos.sencre.tr"
86
+ ).replace(/\/$/, "");
87
+ }
88
+
89
+ function authKey() {
90
+ const s = loadSession();
91
+ if (s?.token) return s.token;
92
+ return process.env.SENCRE_AGENT_KEY || process.env.APPWRITE_API_KEY || "";
93
+ }
94
+
95
+ async function agentFetch(path, init) {
96
+ const token = authKey();
97
+ if (!token && !path.includes("/pair/")) {
98
+ throw new Error(
99
+ "Not logged in. Call the login tool (or /sencre login) first.",
100
+ );
101
+ }
102
+ const res = await fetch(`${apiBase()}${path}`, {
103
+ ...init,
104
+ headers: {
105
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
106
+ "Content-Type": "application/json",
107
+ ...(init?.headers || {}),
108
+ },
109
+ });
110
+ const data = await res.json();
111
+ if (!res.ok || data.ok === false) {
112
+ throw new Error(String(data.error || `HTTP ${res.status}`));
113
+ }
114
+ return data;
115
+ }
116
+
117
+ async function publicFetch(path, init) {
118
+ const res = await fetch(`${apiBase()}${path}`, {
119
+ ...init,
120
+ headers: {
121
+ "Content-Type": "application/json",
122
+ ...(init?.headers || {}),
123
+ },
124
+ });
125
+ const data = await res.json();
126
+ if (!res.ok || data.ok === false) {
127
+ throw new Error(String(data.error || `HTTP ${res.status}`));
128
+ }
129
+ return data;
130
+ }
131
+
132
+ function text(data) {
133
+ return {
134
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
135
+ };
136
+ }
137
+
138
+ function pid(explicit) {
139
+ return (
140
+ explicit?.trim() ||
141
+ loadSession()?.projectId ||
142
+ resolveProjectIdFromCwd() ||
143
+ process.env.SENCRE_PROJECT_ID?.trim() ||
144
+ ""
145
+ );
146
+ }
147
+
148
+ function requirePid(explicit) {
149
+ const id = pid(explicit);
150
+ if (!id) {
151
+ throw new Error(
152
+ "No project bound. Call the login tool first (opens browser).",
153
+ );
154
+ }
155
+ return id;
156
+ }
157
+
158
+ function openBrowser(url) {
159
+ if (process.platform === "win32") {
160
+ exec(`cmd /c start "" "${url}"`, () => undefined);
161
+ } else if (process.platform === "darwin") {
162
+ exec(`open "${url}"`, () => undefined);
163
+ } else {
164
+ exec(`xdg-open "${url}"`, () => undefined);
165
+ }
166
+ }
167
+
168
+ async function main() {
169
+ const server = new McpServer({ name: "sencre", version: "1.3.0" });
170
+
171
+ server.tool(
172
+ "login",
173
+ "Open browser to log in to Sencre and select a project. No API key needed.",
174
+ {
175
+ openBrowser: z.boolean().optional(),
176
+ timeoutSeconds: z.number().optional(),
177
+ },
178
+ async ({ openBrowser: doOpen, timeoutSeconds }) => {
179
+ const start = await publicFetch("/api/agent/pair/start", {
180
+ method: "POST",
181
+ body: "{}",
182
+ });
183
+ if (doOpen !== false) openBrowser(start.url);
184
+ const timeoutMs = (timeoutSeconds ?? 180) * 1000;
185
+ const began = Date.now();
186
+ while (Date.now() - began < timeoutMs) {
187
+ await new Promise((r) => setTimeout(r, 2000));
188
+ const st = await publicFetch(
189
+ `/api/agent/pair/status?code=${encodeURIComponent(start.code)}`,
190
+ );
191
+ if (st.status === "completed" && st.token && st.projectId) {
192
+ saveSession({
193
+ token: st.token,
194
+ projectId: st.projectId,
195
+ apiBase: apiBase(),
196
+ });
197
+ try {
198
+ const board = await agentFetch(
199
+ `/api/agent/board?projectId=${encodeURIComponent(st.projectId)}`,
200
+ );
201
+ saveSession({
202
+ token: st.token,
203
+ projectId: st.projectId,
204
+ projectName: String(board.projectName || ""),
205
+ apiBase: apiBase(),
206
+ });
207
+ } catch {
208
+ /* ok */
209
+ }
210
+ return text({
211
+ ok: true,
212
+ message: "Logged in and project bound",
213
+ projectId: st.projectId,
214
+ sessionFile: sessionFile(),
215
+ url: start.url,
216
+ });
217
+ }
218
+ if (st.status === "expired" || st.status === "invalid") {
219
+ throw new Error(`Pair ${st.status}. Run login again.`);
220
+ }
221
+ }
222
+ return text({
223
+ ok: false,
224
+ message: "Timed out. Open the URL, finish login, run login again.",
225
+ url: start.url,
226
+ code: start.code,
227
+ });
228
+ },
229
+ );
230
+
231
+ server.tool("logout", "Clear local Sencre agent session", {}, async () => {
232
+ clearSession();
233
+ return text({ ok: true, message: "Session cleared", sessionFile: sessionFile() });
234
+ });
235
+
236
+ server.tool(
237
+ "auth_status",
238
+ "Show login status and bound project",
239
+ {},
240
+ async () => {
241
+ const s = loadSession();
242
+ if (!s) {
243
+ return text({
244
+ ok: true,
245
+ loggedIn: false,
246
+ message: "Not logged in. Call login.",
247
+ apiBase: apiBase(),
248
+ });
249
+ }
250
+ return text({
251
+ ok: true,
252
+ loggedIn: true,
253
+ projectId: s.projectId,
254
+ projectName: s.projectName,
255
+ sessionFile: sessionFile(),
256
+ apiBase: apiBase(),
257
+ });
258
+ },
259
+ );
260
+
261
+ server.tool(
262
+ "list_projects",
263
+ "List projects for logged-in user",
264
+ {},
265
+ async () => text(await agentFetch("/api/agent/projects")),
266
+ );
267
+
268
+ server.tool(
269
+ "get_board",
270
+ "Get board for bound project",
271
+ { projectId: z.string().optional() },
272
+ async ({ projectId }) => {
273
+ const id = requirePid(projectId);
274
+ return text(
275
+ await agentFetch(
276
+ `/api/agent/board?projectId=${encodeURIComponent(id)}`,
277
+ ),
278
+ );
279
+ },
280
+ );
281
+
282
+ server.tool(
283
+ "list_tasks",
284
+ "List tasks",
285
+ {
286
+ projectId: z.string().optional(),
287
+ column: z.string().optional(),
288
+ type: z.string().optional(),
289
+ source: z.string().optional(),
290
+ },
291
+ async (args) => {
292
+ const id = requirePid(args.projectId);
293
+ const q = new URLSearchParams({ projectId: id });
294
+ if (args.column) q.set("column", args.column);
295
+ if (args.type) q.set("type", args.type);
296
+ if (args.source) q.set("source", args.source);
297
+ return text(await agentFetch(`/api/agent/tasks?${q}`));
298
+ },
299
+ );
300
+
301
+ server.tool(
302
+ "create_task",
303
+ "Create task (source=agent). Optional: type, priority, estimateHours, startAt, dueAt (ISO).",
304
+ {
305
+ projectId: z.string().optional(),
306
+ title: z.string(),
307
+ description: z.string().optional(),
308
+ column: z.string().optional(),
309
+ type: z.enum(["task", "deficiency"]).optional(),
310
+ priority: z.enum(["none", "low", "medium", "high"]).optional(),
311
+ estimateHours: z.number().optional(),
312
+ startAt: z.string().nullable().optional(),
313
+ dueAt: z.string().nullable().optional(),
314
+ },
315
+ async (args) => {
316
+ const projectId = requirePid(args.projectId);
317
+ return text(
318
+ await agentFetch("/api/agent/tasks", {
319
+ method: "POST",
320
+ body: JSON.stringify({
321
+ projectId,
322
+ title: args.title,
323
+ description: args.description,
324
+ column: args.column,
325
+ type: args.type,
326
+ priority: args.priority,
327
+ estimateHours: args.estimateHours,
328
+ startAt: args.startAt,
329
+ dueAt: args.dueAt,
330
+ }),
331
+ }),
332
+ );
333
+ },
334
+ );
335
+
336
+ server.tool(
337
+ "update_task",
338
+ "Update task fields including type, priority, estimateHours, startAt, dueAt (null clears dates).",
339
+ {
340
+ id: z.string(),
341
+ title: z.string().optional(),
342
+ description: z.string().optional(),
343
+ type: z.enum(["task", "deficiency"]).optional(),
344
+ priority: z.enum(["none", "low", "medium", "high"]).optional(),
345
+ estimateHours: z.number().nullable().optional(),
346
+ startAt: z.string().nullable().optional(),
347
+ dueAt: z.string().nullable().optional(),
348
+ },
349
+ async (args) => {
350
+ const { id, ...patch } = args;
351
+ return text(
352
+ await agentFetch(
353
+ `/api/agent/tasks/by-id?id=${encodeURIComponent(id)}`,
354
+ { method: "PATCH", body: JSON.stringify(patch) },
355
+ ),
356
+ );
357
+ },
358
+ );
359
+
360
+ server.tool(
361
+ "move_task",
362
+ "Move task to column",
363
+ { id: z.string(), column: z.string() },
364
+ async ({ id, column }) =>
365
+ text(
366
+ await agentFetch(
367
+ `/api/agent/tasks/by-id?id=${encodeURIComponent(id)}&action=move`,
368
+ { method: "POST", body: JSON.stringify({ column }) },
369
+ ),
370
+ ),
371
+ );
372
+
373
+ server.tool(
374
+ "complete_task",
375
+ "Move task to Done",
376
+ { id: z.string() },
377
+ async ({ id }) =>
378
+ text(
379
+ await agentFetch(
380
+ `/api/agent/tasks/by-id?id=${encodeURIComponent(id)}&action=complete`,
381
+ { method: "POST", body: "{}" },
382
+ ),
383
+ ),
384
+ );
385
+
386
+ server.tool(
387
+ "whats_left",
388
+ "Open tasks numbered",
389
+ { projectId: z.string().optional() },
390
+ async ({ projectId }) => {
391
+ const id = requirePid(projectId);
392
+ return text(
393
+ await agentFetch(
394
+ `/api/agent/whats-left?projectId=${encodeURIComponent(id)}`,
395
+ ),
396
+ );
397
+ },
398
+ );
399
+
400
+ const transport = new StdioServerTransport();
401
+ await server.connect(transport);
402
+ logErr("ready", apiBase());
403
+ }
404
+
405
+ main().catch((err) => {
406
+ logErr("fatal", err);
407
+ process.exit(1);
408
+ });
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "sencre-mcp",
3
+ "version": "1.3.0",
4
+ "description": "Sencre Kanban MCP server — pair login, board tools for coding agents",
5
+ "homepage": "https://todos.sencre.tr",
6
+ "author": "fraunhofer",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "type": "module",
11
+ "bin": {
12
+ "sencre-mcp": "index.mjs"
13
+ },
14
+ "files": [
15
+ "index.mjs",
16
+ "README.md"
17
+ ],
18
+ "engines": {
19
+ "node": ">=18"
20
+ },
21
+ "scripts": {
22
+ "start": "node index.mjs"
23
+ },
24
+ "keywords": [
25
+ "mcp",
26
+ "sencre",
27
+ "kanban",
28
+ "agent"
29
+ ],
30
+ "license": "MIT",
31
+ "dependencies": {
32
+ "@modelcontextprotocol/sdk": "^1.12.1",
33
+ "zod": "^3.24.0"
34
+ }
35
+ }