secondbrainos-mcp-server 1.2.6 → 1.4.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/build/index.js +182 -2
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -5,6 +5,9 @@ import { OpenApi, HttpLlm } from "@samchon/openapi";
|
|
|
5
5
|
import axios from "axios";
|
|
6
6
|
import dotenv from "dotenv";
|
|
7
7
|
import yaml from 'js-yaml';
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { homedir } from 'os';
|
|
8
11
|
dotenv.config();
|
|
9
12
|
function toSnakeCase(str) {
|
|
10
13
|
return str
|
|
@@ -43,6 +46,7 @@ class SecondBrainOSServer {
|
|
|
43
46
|
// Discover service paths from the OpenAPI schema
|
|
44
47
|
this.runPromptChainPath = null;
|
|
45
48
|
this.getAIAgentsSchemaPath = null;
|
|
49
|
+
this.generateUploadURLPath = null;
|
|
46
50
|
if (initialSchema.paths) {
|
|
47
51
|
for (const [path, pathItem] of Object.entries(initialSchema.paths)) {
|
|
48
52
|
for (const operation of Object.values(pathItem)) {
|
|
@@ -52,8 +56,11 @@ class SecondBrainOSServer {
|
|
|
52
56
|
if (operation.operationId === 'getAIAgentsSchema') {
|
|
53
57
|
this.getAIAgentsSchemaPath = path;
|
|
54
58
|
}
|
|
59
|
+
if (operation.operationId === 'generateFileUploadGoogleCloudStorageURL') {
|
|
60
|
+
this.generateUploadURLPath = path;
|
|
61
|
+
}
|
|
55
62
|
}
|
|
56
|
-
if (this.runPromptChainPath && this.getAIAgentsSchemaPath)
|
|
63
|
+
if (this.runPromptChainPath && this.getAIAgentsSchemaPath && this.generateUploadURLPath)
|
|
57
64
|
break;
|
|
58
65
|
}
|
|
59
66
|
}
|
|
@@ -131,6 +138,8 @@ class SecondBrainOSServer {
|
|
|
131
138
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown function: ${request.params.name}`);
|
|
132
139
|
}
|
|
133
140
|
try {
|
|
141
|
+
// Intercept file_path arguments: upload local files to GCS
|
|
142
|
+
const processedArgs = await this.interceptFilePathArgs((request.params.arguments || {}));
|
|
134
143
|
// Use HttpLlm.execute for better error handling and type safety
|
|
135
144
|
const result = await HttpLlm.execute({
|
|
136
145
|
connection: {
|
|
@@ -141,7 +150,7 @@ class SecondBrainOSServer {
|
|
|
141
150
|
},
|
|
142
151
|
application: this.application, // Type assertion to avoid generic issues
|
|
143
152
|
function: func,
|
|
144
|
-
input:
|
|
153
|
+
input: processedArgs
|
|
145
154
|
});
|
|
146
155
|
return {
|
|
147
156
|
content: [{
|
|
@@ -456,6 +465,71 @@ class SecondBrainOSServer {
|
|
|
456
465
|
});
|
|
457
466
|
return response.data;
|
|
458
467
|
}
|
|
468
|
+
/**
|
|
469
|
+
* Scans tool arguments for file_path parameters containing local paths,
|
|
470
|
+
* uploads those files to GCS via signed URL, and replaces the values with GCS paths.
|
|
471
|
+
* - https:// paths are passed through
|
|
472
|
+
* - http:// paths are rejected
|
|
473
|
+
* - Everything else is treated as a local file path
|
|
474
|
+
*/
|
|
475
|
+
async interceptFilePathArgs(args) {
|
|
476
|
+
const result = { ...args };
|
|
477
|
+
for (const [key, value] of Object.entries(result)) {
|
|
478
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
479
|
+
result[key] = await this.interceptFilePathArgs(value);
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
if (typeof value !== 'string' || !key.toLowerCase().includes('file_path')) {
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
const trimmed = value.trim();
|
|
486
|
+
// https:// — pass through
|
|
487
|
+
if (trimmed.startsWith('https://'))
|
|
488
|
+
continue;
|
|
489
|
+
// http:// — reject
|
|
490
|
+
if (trimmed.startsWith('http://')) {
|
|
491
|
+
throw new McpError(ErrorCode.InvalidRequest, `Insecure http:// URLs are not allowed for file_path. Use https:// or a local file path.`);
|
|
492
|
+
}
|
|
493
|
+
// Local file path — validate, upload via signed URL, replace
|
|
494
|
+
if (!this.generateUploadURLPath) {
|
|
495
|
+
throw new McpError(ErrorCode.InternalError, 'File upload service (generateFileUploadGoogleCloudStorageURL) is not available for this user. Cannot upload local files.');
|
|
496
|
+
}
|
|
497
|
+
const ext = path.extname(trimmed).toLowerCase();
|
|
498
|
+
if (!SecondBrainOSServer.ALLOWED_EXTENSIONS.includes(ext)) {
|
|
499
|
+
throw new McpError(ErrorCode.InvalidRequest, `Unsupported file extension "${ext}" for file_path. Only ${SecondBrainOSServer.ALLOWED_EXTENSIONS.join(', ')} are supported.`);
|
|
500
|
+
}
|
|
501
|
+
if (!fs.existsSync(trimmed)) {
|
|
502
|
+
throw new McpError(ErrorCode.InvalidRequest, `Local file not found: ${trimmed}`);
|
|
503
|
+
}
|
|
504
|
+
const fileContent = fs.readFileSync(trimmed, 'utf-8');
|
|
505
|
+
if (!fileContent.trim()) {
|
|
506
|
+
throw new McpError(ErrorCode.InvalidRequest, `File is empty: ${trimmed}`);
|
|
507
|
+
}
|
|
508
|
+
const fileName = path.basename(trimmed);
|
|
509
|
+
const fileSize = Buffer.byteLength(fileContent, 'utf-8');
|
|
510
|
+
// Step 1: Get signed upload URL from the GCF
|
|
511
|
+
const uploadUrlResponse = await axios.post(`${this.baseUrl}${this.generateUploadURLPath}`, {
|
|
512
|
+
file_name: fileName,
|
|
513
|
+
file_size: fileSize,
|
|
514
|
+
mime_type: 'text/plain'
|
|
515
|
+
}, {
|
|
516
|
+
headers: {
|
|
517
|
+
'Authorization': `Bearer ${this.userId}:${this.userSecret}`,
|
|
518
|
+
'Content-Type': 'application/json'
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
const { upload_url, gs_path } = uploadUrlResponse.data;
|
|
522
|
+
// Step 2: PUT file content directly to GCS via signed URL
|
|
523
|
+
await axios.put(upload_url, fileContent, {
|
|
524
|
+
headers: {
|
|
525
|
+
'Content-Type': 'text/plain'
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
console.error(`Uploaded local file to ${gs_path}`);
|
|
529
|
+
result[key] = gs_path;
|
|
530
|
+
}
|
|
531
|
+
return result;
|
|
532
|
+
}
|
|
459
533
|
async fetchAndEnrichAgents() {
|
|
460
534
|
if (this.cachedAgents)
|
|
461
535
|
return this.cachedAgents;
|
|
@@ -489,6 +563,109 @@ class SecondBrainOSServer {
|
|
|
489
563
|
this.cachedAgents = agents;
|
|
490
564
|
return agents;
|
|
491
565
|
}
|
|
566
|
+
/**
|
|
567
|
+
* Write Claude Code skill files to ~/.claude/skills/secondbrainos/
|
|
568
|
+
* Called on every server startup so skills stay in sync with SBOS.
|
|
569
|
+
*/
|
|
570
|
+
async writeSkillFiles() {
|
|
571
|
+
const skillsBase = path.join(homedir(), '.claude', 'skills', 'secondbrainos');
|
|
572
|
+
// Helper to write a SKILL.md file
|
|
573
|
+
const writeSkill = (dir, content) => {
|
|
574
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
575
|
+
fs.writeFileSync(path.join(dir, 'SKILL.md'), content, 'utf-8');
|
|
576
|
+
};
|
|
577
|
+
// 1. server-use — placeholder
|
|
578
|
+
writeSkill(path.join(skillsBase, 'server-use'), `---\nname: secondbrainos_server_use\ndescription: Second Brain OS server context\nuser-invocable: false\n---\n\nHello world.\n`);
|
|
579
|
+
// 2. tabs (agents)
|
|
580
|
+
if (this.getAIAgentsSchemaPath) {
|
|
581
|
+
try {
|
|
582
|
+
const agents = await this.fetchAndEnrichAgents();
|
|
583
|
+
for (const agent of agents) {
|
|
584
|
+
if (!agent.name || !agent.id)
|
|
585
|
+
continue;
|
|
586
|
+
const folderName = toSnakeCase(agent.name);
|
|
587
|
+
const agentDocument = {
|
|
588
|
+
agent_id: agent.id,
|
|
589
|
+
name: agent.name,
|
|
590
|
+
description: agent.description || '',
|
|
591
|
+
behaviour_and_instructions: agent.behaviour_and_instructions || '',
|
|
592
|
+
searchMyKnowledge_collection_id: agent.searchMyKnowledge_collection_id || '',
|
|
593
|
+
actions: agent.actions || [],
|
|
594
|
+
workflows: (agent.workflows || []).map((wf) => ({
|
|
595
|
+
id: wf.id,
|
|
596
|
+
name: wf.name,
|
|
597
|
+
prompts: (wf.prompts || []).map((p) => ({
|
|
598
|
+
id: p.id,
|
|
599
|
+
name: p.name,
|
|
600
|
+
order: p.order,
|
|
601
|
+
description: p.description
|
|
602
|
+
}))
|
|
603
|
+
}))
|
|
604
|
+
};
|
|
605
|
+
writeSkill(path.join(skillsBase, 'tabs', folderName), `---\nname: secondbrainos_agent_${folderName}\ndescription: "${agent.description || agent.name}"\nuser-invocable: false\n---\n\n${JSON.stringify(agentDocument, null, 2)}\n`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
catch (error) {
|
|
609
|
+
console.error('Failed to write agent skill files:', error);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
// 3. workflows
|
|
613
|
+
if (this.runPromptChainPath) {
|
|
614
|
+
try {
|
|
615
|
+
const data = await this.callRunPromptChain('', '');
|
|
616
|
+
const workflows = data.workflows || [];
|
|
617
|
+
for (const wf of workflows) {
|
|
618
|
+
if (!wf.name || !wf.workflow_id)
|
|
619
|
+
continue;
|
|
620
|
+
const folderName = toSnakeCase(wf.name);
|
|
621
|
+
const workflowDetail = await this.callRunPromptChain('workflow', wf.workflow_id);
|
|
622
|
+
const promptIds = workflowDetail.prompt_id || [];
|
|
623
|
+
const promptResults = await Promise.all(promptIds.map(async (promptId) => {
|
|
624
|
+
try {
|
|
625
|
+
const promptData = await this.callRunPromptChain('prompt', promptId);
|
|
626
|
+
return {
|
|
627
|
+
id: promptId,
|
|
628
|
+
name: promptData.name,
|
|
629
|
+
order: promptData.order,
|
|
630
|
+
description: promptData.description || ''
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
catch {
|
|
634
|
+
return { id: promptId, name: '', order: 0, description: '' };
|
|
635
|
+
}
|
|
636
|
+
}));
|
|
637
|
+
const sortedPrompts = promptResults.sort((a, b) => (a.order || 0) - (b.order || 0));
|
|
638
|
+
const workflowDocument = {
|
|
639
|
+
skill_id: wf.workflow_id,
|
|
640
|
+
name: wf.name,
|
|
641
|
+
description: wf.description || '',
|
|
642
|
+
prompts: sortedPrompts
|
|
643
|
+
};
|
|
644
|
+
writeSkill(path.join(skillsBase, 'workflows', folderName), `---\nname: secondbrainos_skill_${folderName}\ndescription: "${wf.description || wf.name}"\nuser-invocable: false\n---\n\n${JSON.stringify(workflowDocument, null, 2)}\n`);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
catch (error) {
|
|
648
|
+
console.error('Failed to write workflow skill files:', error);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
// 4. knowledgebases
|
|
652
|
+
if (this.getAIAgentsSchemaPath) {
|
|
653
|
+
try {
|
|
654
|
+
const agents = await this.fetchAndEnrichAgents();
|
|
655
|
+
const collectionIds = agents
|
|
656
|
+
.map((a) => a.searchMyKnowledge_collection_id)
|
|
657
|
+
.filter((id) => id && id.length > 0);
|
|
658
|
+
if (collectionIds.length > 0) {
|
|
659
|
+
const kbDocument = { searchMyKnowledge_collection_ids: collectionIds };
|
|
660
|
+
writeSkill(path.join(skillsBase, 'knowledgebases'), `---\nname: secondbrainos_knowledge_bases\ndescription: Knowledge base collection IDs available to agents\nuser-invocable: false\n---\n\n${JSON.stringify(kbDocument, null, 2)}\n`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
catch (error) {
|
|
664
|
+
console.error('Failed to write knowledge bases skill file:', error);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
console.error('Skill files written to ~/.claude/skills/secondbrainos/');
|
|
668
|
+
}
|
|
492
669
|
setupErrorHandling() {
|
|
493
670
|
// Error handling is now built into HttpLlm.execute
|
|
494
671
|
// This method is kept for future error handling implementations
|
|
@@ -497,8 +674,11 @@ class SecondBrainOSServer {
|
|
|
497
674
|
const transport = new StdioServerTransport();
|
|
498
675
|
await this.server.connect(transport);
|
|
499
676
|
console.error("Second Brain OS MCP server running on stdio");
|
|
677
|
+
// Write skill files in the background (non-blocking)
|
|
678
|
+
this.writeSkillFiles().catch(err => console.error('Failed to write skill files:', err));
|
|
500
679
|
}
|
|
501
680
|
}
|
|
681
|
+
SecondBrainOSServer.ALLOWED_EXTENSIONS = ['.txt', '.md'];
|
|
502
682
|
// Function to fetch the schema from the API
|
|
503
683
|
async function fetchSchema() {
|
|
504
684
|
const userId = process.env.USER_ID;
|