@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 +124 -0
- package/dist/dayby-client.d.ts +58 -0
- package/dist/dayby-client.js +55 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +285 -0
- package/dist/sanitizer.d.ts +34 -0
- package/dist/sanitizer.js +108 -0
- package/package.json +27 -0
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;
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|