@dayby/mcp-server 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.
package/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # @dayby/mcp-server
2
+
3
+ Post your dev progress to [DayBy.dev](https://dayby.dev) from Claude, Cursor, or any MCP client — with **local sanitization** so your company secrets never leave your machine.
4
+
5
+ ## How It Works
6
+
7
+ ```
8
+ You're coding with Claude → learn something cool →
9
+ draft_post (sanitized locally, never touches network) →
10
+ Claude shows you a clean preview →
11
+ You say "ship it" →
12
+ publish_post → DayBy API (only sanitized content sent)
13
+ ```
14
+
15
+ **The raw context from your codebase never touches the network.** Only the sanitized, approved version gets published.
16
+
17
+ ## Tools
18
+
19
+ | Tool | What it does | Touches network? |
20
+ |---|---|---|
21
+ | `draft_post` | Creates a sanitized draft from your description | ❌ No |
22
+ | `edit_draft` | Modify a draft before publishing | ❌ No |
23
+ | `check_content` | Dry-run: see what would get stripped | ❌ No |
24
+ | `publish_post` | Publish an approved draft to DayBy | ✅ Yes (sanitized only) |
25
+ | `list_posts` | List your recent DayBy posts | ✅ Yes |
26
+
27
+ ## What Gets Stripped (Automatically)
28
+
29
+ - API keys, tokens, secrets
30
+ - AWS ARNs and access keys
31
+ - Private IP addresses
32
+ - Email addresses
33
+ - SSH keys, JWTs, GitHub tokens
34
+ - Database connection URLs
35
+ - File paths with usernames
36
+ - Plus anything you configure in blocklist ↓
37
+
38
+ ## Setup
39
+
40
+ ### 1. Get a DayBy API Key
41
+
42
+ 1. Sign up at [dayby.dev](https://dayby.dev)
43
+ 2. Go to Settings → API
44
+ 3. Enable API access and generate a key
45
+
46
+ ### 2. Configure Sanitizer (Optional but Recommended)
47
+
48
+ Create `~/.dayby/sanitizer.json`:
49
+
50
+ ```json
51
+ {
52
+ "blockedTerms": ["YourCompany", "ProjectCodename"],
53
+ "blockedDomains": ["internal.yourcompany.com"],
54
+ "blockedNames": ["Your Boss Name"],
55
+ "customPatterns": ["JIRA-\\d+", "INTERNAL-\\d+"]
56
+ }
57
+ ```
58
+
59
+ ### 3. Install
60
+
61
+ ```bash
62
+ npm install -g @dayby/mcp-server
63
+ ```
64
+
65
+ ### 4. Add to Claude Code / Claude Desktop / Cursor
66
+
67
+ **Claude Code (simplest):**
68
+ ```bash
69
+ claude mcp add dayby -- dayby-mcp
70
+ ```
71
+
72
+ **Claude Desktop** (`claude_desktop_config.json`):
73
+ ```json
74
+ {
75
+ "mcpServers": {
76
+ "dayby": {
77
+ "command": "dayby-mcp",
78
+ "env": {
79
+ "DAYBY_API_KEY": "your-api-key-here"
80
+ }
81
+ }
82
+ }
83
+ }
84
+ ```
85
+
86
+ **Cursor** (`.cursor/mcp.json`):
87
+ ```json
88
+ {
89
+ "mcpServers": {
90
+ "dayby": {
91
+ "command": "dayby-mcp",
92
+ "env": {
93
+ "DAYBY_API_KEY": "your-api-key-here"
94
+ }
95
+ }
96
+ }
97
+ }
98
+ ```
99
+
100
+ ## Usage Examples
101
+
102
+ **While coding:**
103
+ > "I just figured out how to use PostgreSQL partial indexes to optimize a multi-tenant query. Draft a DayBy post about it."
104
+
105
+ **After a PR:**
106
+ > "I built a rate limiter using Redis sorted sets today. Post it to DayBy."
107
+
108
+ **Quick check:**
109
+ > "Check if this text has any sensitive data before I post it."
110
+
111
+ Claude will use `draft_post` to sanitize locally, show you a preview, and only publish when you approve.
112
+
113
+ ## Environment Variables
114
+
115
+ | Variable | Description | Default |
116
+ |---|---|---|
117
+ | `DAYBY_API_KEY` | Your DayBy API key | (required) |
118
+ | `DAYBY_API_URL` | DayBy API URL | `https://dayby.dev` |
119
+ | `DAYBY_BLOCKED_TERMS` | Comma-separated blocked terms | (none) |
120
+ | `DAYBY_BLOCKED_DOMAINS` | Comma-separated blocked domains | (none) |
121
+
122
+ ## License
123
+
124
+ MIT
@@ -0,0 +1,58 @@
1
+ /**
2
+ * DayBy API v2 client — thin wrapper around the REST API.
3
+ */
4
+ export interface DayByConfig {
5
+ apiUrl: string;
6
+ apiKey: string;
7
+ }
8
+ export interface DayByPost {
9
+ id: number;
10
+ title: string;
11
+ slug: string;
12
+ content: string;
13
+ visibility: string;
14
+ has_article: boolean;
15
+ url: string;
16
+ created_at: string;
17
+ updated_at: string;
18
+ }
19
+ export interface PostsListResponse {
20
+ posts: DayByPost[];
21
+ meta: {
22
+ total: number;
23
+ page: number;
24
+ per_page: number;
25
+ total_pages: number;
26
+ };
27
+ }
28
+ export declare class DayByClient {
29
+ private apiUrl;
30
+ private apiKey;
31
+ constructor(config: DayByConfig);
32
+ private request;
33
+ listPosts(page?: number, perPage?: number): Promise<PostsListResponse>;
34
+ getPost(slug: string): Promise<{
35
+ post: DayByPost;
36
+ }>;
37
+ createPost(params: {
38
+ title: string;
39
+ content: string;
40
+ visibility?: string;
41
+ }): Promise<{
42
+ post: DayByPost;
43
+ }>;
44
+ updatePost(slug: string, params: {
45
+ title?: string;
46
+ content?: string;
47
+ visibility?: string;
48
+ }): Promise<{
49
+ post: DayByPost;
50
+ }>;
51
+ deletePost(slug: string): Promise<{
52
+ message: string;
53
+ }>;
54
+ generateArticle(slug: string): Promise<{
55
+ post: DayByPost;
56
+ message: string;
57
+ }>;
58
+ }
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ /**
3
+ * DayBy API v2 client — thin wrapper around the REST API.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.DayByClient = void 0;
7
+ class DayByClient {
8
+ apiUrl;
9
+ apiKey;
10
+ constructor(config) {
11
+ this.apiUrl = config.apiUrl.replace(/\/$/, '');
12
+ this.apiKey = config.apiKey;
13
+ }
14
+ async request(method, path, body) {
15
+ const url = `${this.apiUrl}${path}`;
16
+ const headers = {
17
+ 'Authorization': `Bearer ${this.apiKey}`,
18
+ 'Content-Type': 'application/json',
19
+ 'Accept': 'application/json',
20
+ };
21
+ const res = await fetch(url, {
22
+ method,
23
+ headers,
24
+ body: body ? JSON.stringify(body) : undefined,
25
+ });
26
+ if (!res.ok) {
27
+ const error = await res.json().catch(() => ({ error: res.statusText }));
28
+ throw new Error(`DayBy API error (${res.status}): ${JSON.stringify(error)}`);
29
+ }
30
+ return res.json();
31
+ }
32
+ async listPosts(page = 1, perPage = 10) {
33
+ return this.request('GET', `/api/v2/posts?page=${page}&per_page=${perPage}`);
34
+ }
35
+ async getPost(slug) {
36
+ return this.request('GET', `/api/v2/posts/${slug}`);
37
+ }
38
+ async createPost(params) {
39
+ return this.request('POST', '/api/v2/posts', {
40
+ post: params,
41
+ });
42
+ }
43
+ async updatePost(slug, params) {
44
+ return this.request('PUT', `/api/v2/posts/${slug}`, {
45
+ post: params,
46
+ });
47
+ }
48
+ async deletePost(slug) {
49
+ return this.request('DELETE', `/api/v2/posts/${slug}`);
50
+ }
51
+ async generateArticle(slug) {
52
+ return this.request('POST', `/api/v2/posts/${slug}/generate_article`);
53
+ }
54
+ }
55
+ exports.DayByClient = DayByClient;
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * DayBy MCP Server
4
+ *
5
+ * Post your dev progress from Claude, Cursor, or any MCP client.
6
+ * All content is sanitized locally before it ever touches the network.
7
+ *
8
+ * Flow: draft_post → review → publish_post (nothing leaves without approval)
9
+ */
10
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,285 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * DayBy MCP Server
5
+ *
6
+ * Post your dev progress from Claude, Cursor, or any MCP client.
7
+ * All content is sanitized locally before it ever touches the network.
8
+ *
9
+ * Flow: draft_post → review → publish_post (nothing leaves without approval)
10
+ */
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
46
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
47
+ const zod_1 = require("zod");
48
+ const sanitizer_js_1 = require("./sanitizer.js");
49
+ const dayby_client_js_1 = require("./dayby-client.js");
50
+ const fs = __importStar(require("fs"));
51
+ const path = __importStar(require("path"));
52
+ function loadConfig() {
53
+ // 1. Check env vars
54
+ const apiUrl = process.env.DAYBY_API_URL || 'https://dayby.dev';
55
+ const apiKey = process.env.DAYBY_API_KEY || '';
56
+ // 2. Load sanitizer config from file if it exists
57
+ let sanitizerConfig = {};
58
+ const configPaths = [
59
+ path.join(process.env.HOME || '~', '.dayby', 'sanitizer.json'),
60
+ path.join(process.cwd(), '.dayby-sanitizer.json'),
61
+ ];
62
+ for (const configPath of configPaths) {
63
+ try {
64
+ const raw = fs.readFileSync(configPath, 'utf-8');
65
+ sanitizerConfig = JSON.parse(raw);
66
+ break;
67
+ }
68
+ catch {
69
+ // File doesn't exist, that's fine
70
+ }
71
+ }
72
+ // 3. Also load from env (comma-separated)
73
+ if (process.env.DAYBY_BLOCKED_TERMS) {
74
+ sanitizerConfig.blockedTerms = [
75
+ ...(sanitizerConfig.blockedTerms || []),
76
+ ...process.env.DAYBY_BLOCKED_TERMS.split(',').map(s => s.trim()),
77
+ ];
78
+ }
79
+ if (process.env.DAYBY_BLOCKED_DOMAINS) {
80
+ sanitizerConfig.blockedDomains = [
81
+ ...(sanitizerConfig.blockedDomains || []),
82
+ ...process.env.DAYBY_BLOCKED_DOMAINS.split(',').map(s => s.trim()),
83
+ ];
84
+ }
85
+ return { apiUrl, apiKey, sanitizer: sanitizerConfig };
86
+ }
87
+ const drafts = new Map();
88
+ function generateDraftId() {
89
+ return `draft_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
90
+ }
91
+ // --- Main ---
92
+ async function main() {
93
+ const config = loadConfig();
94
+ const sanitizer = new sanitizer_js_1.Sanitizer(config.sanitizer);
95
+ const client = new dayby_client_js_1.DayByClient({ apiUrl: config.apiUrl, apiKey: config.apiKey });
96
+ const server = new mcp_js_1.McpServer({
97
+ name: 'dayby',
98
+ version: '0.1.0',
99
+ });
100
+ // ========================================
101
+ // Tool: draft_post
102
+ // Sanitizes content locally, returns preview. NOTHING leaves the machine.
103
+ // ========================================
104
+ server.tool('draft_post', 'Create a sanitized draft of a dev progress post. Content is cleaned locally — nothing is sent to the network. Returns a preview for the user to review before publishing.', {
105
+ title: zod_1.z.string().describe('Post title — focus on the technology/skill learned'),
106
+ content: zod_1.z.string().describe('Post content — describe what you learned, built, or solved. The sanitizer will strip any sensitive data automatically.'),
107
+ visibility: zod_1.z.enum(['published', 'draft']).default('published').describe('Post visibility on DayBy'),
108
+ }, async ({ title, content, visibility }) => {
109
+ // Sanitize both title and content locally
110
+ const titleResult = sanitizer.sanitize(title);
111
+ const contentResult = sanitizer.sanitize(content);
112
+ const allStripped = [...titleResult.stripped, ...contentResult.stripped];
113
+ const draftId = generateDraftId();
114
+ const draft = {
115
+ id: draftId,
116
+ originalContent: content,
117
+ sanitizedTitle: titleResult.clean,
118
+ sanitizedContent: contentResult.clean,
119
+ strippedItems: allStripped,
120
+ createdAt: new Date(),
121
+ };
122
+ drafts.set(draftId, draft);
123
+ // Build response
124
+ let response = `📝 **Draft Preview** (ID: ${draftId})\n\n`;
125
+ response += `**Title:** ${draft.sanitizedTitle}\n\n`;
126
+ response += `**Content:**\n${draft.sanitizedContent}\n\n`;
127
+ response += `**Visibility:** ${visibility}\n`;
128
+ if (allStripped.length > 0) {
129
+ response += `\n⚠️ **Sanitizer removed ${allStripped.length} sensitive item(s):**\n`;
130
+ for (const item of allStripped.slice(0, 10)) {
131
+ response += ` • ${item}\n`;
132
+ }
133
+ if (allStripped.length > 10) {
134
+ response += ` • ...and ${allStripped.length - 10} more\n`;
135
+ }
136
+ }
137
+ else {
138
+ response += `\n✅ No sensitive data detected.\n`;
139
+ }
140
+ response += `\n👉 Review the above. If it looks good, use **publish_post** with draft ID: \`${draftId}\``;
141
+ response += `\n💡 You can also use **edit_draft** to modify before publishing.`;
142
+ return { content: [{ type: 'text', text: response }] };
143
+ });
144
+ // ========================================
145
+ // Tool: edit_draft
146
+ // Modify a draft before publishing
147
+ // ========================================
148
+ server.tool('edit_draft', 'Edit a draft post before publishing. Provide updated title and/or content — they will be re-sanitized locally.', {
149
+ draft_id: zod_1.z.string().describe('The draft ID from draft_post'),
150
+ title: zod_1.z.string().optional().describe('Updated title (will be re-sanitized)'),
151
+ content: zod_1.z.string().optional().describe('Updated content (will be re-sanitized)'),
152
+ }, async ({ draft_id, title, content }) => {
153
+ const draft = drafts.get(draft_id);
154
+ if (!draft) {
155
+ return {
156
+ content: [{ type: 'text', text: `❌ Draft not found: ${draft_id}. Use draft_post to create a new one.` }],
157
+ };
158
+ }
159
+ if (title) {
160
+ const result = sanitizer.sanitize(title);
161
+ draft.sanitizedTitle = result.clean;
162
+ draft.strippedItems.push(...result.stripped);
163
+ }
164
+ if (content) {
165
+ const result = sanitizer.sanitize(content);
166
+ draft.sanitizedContent = result.clean;
167
+ draft.originalContent = content;
168
+ draft.strippedItems.push(...result.stripped);
169
+ }
170
+ let response = `✏️ **Draft Updated** (ID: ${draft_id})\n\n`;
171
+ response += `**Title:** ${draft.sanitizedTitle}\n\n`;
172
+ response += `**Content:**\n${draft.sanitizedContent}\n\n`;
173
+ response += `\n👉 Use **publish_post** with draft ID: \`${draft_id}\` when ready.`;
174
+ return { content: [{ type: 'text', text: response }] };
175
+ });
176
+ // ========================================
177
+ // Tool: publish_post
178
+ // Only NOW does data leave the machine — and only the sanitized version.
179
+ // ========================================
180
+ server.tool('publish_post', 'Publish a previously drafted post to DayBy. Only the sanitized version is sent — the original content never leaves your machine.', {
181
+ draft_id: zod_1.z.string().describe('The draft ID from draft_post'),
182
+ generate_article: zod_1.z.boolean().default(false).describe('Also generate an AI-formatted article on DayBy after publishing'),
183
+ }, async ({ draft_id, generate_article }) => {
184
+ const draft = drafts.get(draft_id);
185
+ if (!draft) {
186
+ return {
187
+ content: [{ type: 'text', text: `❌ Draft not found: ${draft_id}. Use draft_post to create a new one.` }],
188
+ };
189
+ }
190
+ if (!config.apiKey) {
191
+ return {
192
+ content: [{
193
+ type: 'text',
194
+ text: '❌ No API key configured. Set DAYBY_API_KEY environment variable or add it to your MCP config.',
195
+ }],
196
+ };
197
+ }
198
+ try {
199
+ // Only the sanitized content is sent
200
+ const result = await client.createPost({
201
+ title: draft.sanitizedTitle,
202
+ content: draft.sanitizedContent,
203
+ });
204
+ let response = `✅ **Published to DayBy!**\n\n`;
205
+ response += `**Title:** ${result.post.title}\n`;
206
+ response += `**URL:** ${result.post.url}\n`;
207
+ response += `**Slug:** ${result.post.slug}\n`;
208
+ if (generate_article && result.post.slug) {
209
+ try {
210
+ await client.generateArticle(result.post.slug);
211
+ response += `\n🤖 AI article generation triggered.`;
212
+ }
213
+ catch (e) {
214
+ response += `\n⚠️ Article generation failed: ${e instanceof Error ? e.message : 'Unknown error'}`;
215
+ }
216
+ }
217
+ // Clean up draft
218
+ drafts.delete(draft_id);
219
+ return { content: [{ type: 'text', text: response }] };
220
+ }
221
+ catch (e) {
222
+ return {
223
+ content: [{
224
+ type: 'text',
225
+ text: `❌ Failed to publish: ${e instanceof Error ? e.message : 'Unknown error'}`,
226
+ }],
227
+ };
228
+ }
229
+ });
230
+ // ========================================
231
+ // Tool: list_posts
232
+ // ========================================
233
+ server.tool('list_posts', 'List your recent DayBy posts.', {
234
+ page: zod_1.z.number().default(1).describe('Page number'),
235
+ per_page: zod_1.z.number().default(10).describe('Posts per page'),
236
+ }, async ({ page, per_page }) => {
237
+ if (!config.apiKey) {
238
+ return {
239
+ content: [{ type: 'text', text: '❌ No API key configured.' }],
240
+ };
241
+ }
242
+ try {
243
+ const result = await client.listPosts(page, per_page);
244
+ let response = `📋 **Your DayBy Posts** (page ${result.meta.page}/${result.meta.total_pages}, ${result.meta.total} total)\n\n`;
245
+ for (const post of result.posts) {
246
+ response += `• **${post.title}** — ${post.url}\n`;
247
+ response += ` ${post.content.slice(0, 100)}${post.content.length > 100 ? '...' : ''}\n`;
248
+ response += ` _${post.created_at.slice(0, 10)} · ${post.visibility}_\n\n`;
249
+ }
250
+ return { content: [{ type: 'text', text: response }] };
251
+ }
252
+ catch (e) {
253
+ return {
254
+ content: [{
255
+ type: 'text',
256
+ text: `❌ Failed to list posts: ${e instanceof Error ? e.message : 'Unknown error'}`,
257
+ }],
258
+ };
259
+ }
260
+ });
261
+ // ========================================
262
+ // Tool: check_content
263
+ // Dry-run sanitization — see what would get stripped without creating a draft.
264
+ // ========================================
265
+ server.tool('check_content', 'Check if content contains sensitive data without creating a draft. Useful for quick checks before writing a post.', {
266
+ text: zod_1.z.string().describe('Text to check for sensitive content'),
267
+ }, async ({ text }) => {
268
+ const result = sanitizer.check(text);
269
+ if (result.safe) {
270
+ return {
271
+ content: [{ type: 'text', text: '✅ No sensitive data detected. Safe to post.' }],
272
+ };
273
+ }
274
+ let response = `⚠️ **Found ${result.issues.length} sensitive item(s):**\n\n`;
275
+ for (const issue of result.issues) {
276
+ response += ` • ${issue}\n`;
277
+ }
278
+ response += `\nUse **draft_post** to create a sanitized version.`;
279
+ return { content: [{ type: 'text', text: response }] };
280
+ });
281
+ // --- Start server ---
282
+ const transport = new stdio_js_1.StdioServerTransport();
283
+ await server.connect(transport);
284
+ }
285
+ main().catch(console.error);
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Local sanitization layer — strips sensitive data BEFORE anything leaves the machine.
3
+ * Runs entirely offline. No network calls.
4
+ */
5
+ export interface SanitizerConfig {
6
+ /** Company/org names to strip */
7
+ blockedTerms: string[];
8
+ /** Internal domains (e.g., "internal.corp.com") */
9
+ blockedDomains: string[];
10
+ /** Teammate/people names to strip */
11
+ blockedNames: string[];
12
+ /** Custom regex patterns to strip */
13
+ customPatterns: string[];
14
+ }
15
+ export declare class Sanitizer {
16
+ private config;
17
+ private compiledBlockedTerms;
18
+ private compiledCustomPatterns;
19
+ constructor(config?: Partial<SanitizerConfig>);
20
+ /**
21
+ * Sanitize text locally. Returns the cleaned text and a list of what was stripped.
22
+ */
23
+ sanitize(text: string): {
24
+ clean: string;
25
+ stripped: string[];
26
+ };
27
+ /**
28
+ * Check if text contains any blocked content without modifying it.
29
+ */
30
+ check(text: string): {
31
+ safe: boolean;
32
+ issues: string[];
33
+ };
34
+ }
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ /**
3
+ * Local sanitization layer — strips sensitive data BEFORE anything leaves the machine.
4
+ * Runs entirely offline. No network calls.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.Sanitizer = void 0;
8
+ const DEFAULT_PATTERNS = [
9
+ // API keys & tokens
10
+ /(?:api[_-]?key|token|secret|password|bearer)\s*[:=]\s*['"]?[A-Za-z0-9_\-/.+]{16,}['"]?/gi,
11
+ // AWS ARNs
12
+ /arn:aws:[a-z0-9\-]+:[a-z0-9\-]*:\d{12}:[a-zA-Z0-9\-_/:.]+/g,
13
+ // AWS access keys
14
+ /(?:AKIA|ABIA|ACCA|ASIA)[A-Z0-9]{16}/g,
15
+ // Private IPs
16
+ /\b(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})\b/g,
17
+ // Email addresses
18
+ /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
19
+ // SSH keys
20
+ /ssh-(?:rsa|ed25519|ecdsa)\s+[A-Za-z0-9+/=]{40,}/g,
21
+ // JWT tokens
22
+ /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]+/g,
23
+ // GitHub tokens
24
+ /gh[ps]_[A-Za-z0-9_]{36,}/g,
25
+ // Generic secrets in env-like format
26
+ /[A-Z_]{3,}=["']?[A-Za-z0-9+/=_\-]{20,}["']?/g,
27
+ // Database URLs
28
+ /(?:postgres|mysql|mongodb|redis):\/\/[^\s"']+/gi,
29
+ // File paths with usernames
30
+ /\/(?:home|Users)\/[a-zA-Z0-9._-]+/g,
31
+ ];
32
+ class Sanitizer {
33
+ config;
34
+ compiledBlockedTerms;
35
+ compiledCustomPatterns;
36
+ constructor(config = {}) {
37
+ this.config = {
38
+ blockedTerms: config.blockedTerms || [],
39
+ blockedDomains: config.blockedDomains || [],
40
+ blockedNames: config.blockedNames || [],
41
+ customPatterns: config.customPatterns || [],
42
+ };
43
+ // Compile blocked terms into case-insensitive regexes with word boundaries
44
+ this.compiledBlockedTerms = [
45
+ ...this.config.blockedTerms,
46
+ ...this.config.blockedNames,
47
+ ]
48
+ .filter(t => t.length > 0)
49
+ .map(term => new RegExp(`\\b${escapeRegex(term)}\\b`, 'gi'));
50
+ this.compiledCustomPatterns = this.config.customPatterns
51
+ .filter(p => p.length > 0)
52
+ .map(p => new RegExp(p, 'gi'));
53
+ }
54
+ /**
55
+ * Sanitize text locally. Returns the cleaned text and a list of what was stripped.
56
+ */
57
+ sanitize(text) {
58
+ const stripped = [];
59
+ let clean = text;
60
+ // 1. Strip default sensitive patterns
61
+ for (const pattern of DEFAULT_PATTERNS) {
62
+ const matches = clean.match(pattern);
63
+ if (matches) {
64
+ stripped.push(...matches.map(m => `[pattern: ${m.slice(0, 20)}...]`));
65
+ clean = clean.replace(pattern, '[REDACTED]');
66
+ }
67
+ }
68
+ // 2. Strip blocked domains
69
+ for (const domain of this.config.blockedDomains) {
70
+ const domainRegex = new RegExp(`(?:https?://)?(?:[a-z0-9-]+\\.)*${escapeRegex(domain)}[/\\w.-]*`, 'gi');
71
+ const matches = clean.match(domainRegex);
72
+ if (matches) {
73
+ stripped.push(...matches.map(m => `[domain: ${domain}]`));
74
+ clean = clean.replace(domainRegex, '[REDACTED-URL]');
75
+ }
76
+ }
77
+ // 3. Strip blocked terms and names
78
+ for (const termRegex of this.compiledBlockedTerms) {
79
+ const matches = clean.match(termRegex);
80
+ if (matches) {
81
+ stripped.push(...matches.map(m => `[term: ${m}]`));
82
+ clean = clean.replace(termRegex, '[REDACTED]');
83
+ }
84
+ }
85
+ // 4. Strip custom patterns
86
+ for (const pattern of this.compiledCustomPatterns) {
87
+ const matches = clean.match(pattern);
88
+ if (matches) {
89
+ stripped.push(...matches.map(m => `[custom: ${m.slice(0, 20)}...]`));
90
+ clean = clean.replace(pattern, '[REDACTED]');
91
+ }
92
+ }
93
+ // 5. Clean up multiple consecutive [REDACTED] tags
94
+ clean = clean.replace(/(\[REDACTED(?:-URL)?\]\s*){2,}/g, '[REDACTED] ');
95
+ return { clean: clean.trim(), stripped };
96
+ }
97
+ /**
98
+ * Check if text contains any blocked content without modifying it.
99
+ */
100
+ check(text) {
101
+ const { stripped } = this.sanitize(text);
102
+ return { safe: stripped.length === 0, issues: stripped };
103
+ }
104
+ }
105
+ exports.Sanitizer = Sanitizer;
106
+ function escapeRegex(str) {
107
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
108
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@dayby/mcp-server",
3
+ "version": "0.1.0",
4
+ "description": "DayBy MCP Server — Post your dev progress from Claude, Cursor, or any MCP client. Local sanitization built in.",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "dayby-mcp": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist/",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "dev": "tsc --watch",
16
+ "start": "node dist/index.js"
17
+ },
18
+ "keywords": ["mcp", "dayby", "developer", "journal", "claude"],
19
+ "license": "MIT",
20
+ "dependencies": {
21
+ "@modelcontextprotocol/sdk": "^1.0.0"
22
+ },
23
+ "devDependencies": {
24
+ "typescript": "^5.3.0",
25
+ "@types/node": "^20.0.0"
26
+ }
27
+ }