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.
Files changed (2) hide show
  1. package/build/index.js +182 -2
  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: request.params.arguments || {}
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "secondbrainos-mcp-server",
3
- "version": "1.2.6",
3
+ "version": "1.4.0",
4
4
  "description": "Second Brain OS MCP Server for Claude Desktop",
5
5
  "type": "module",
6
6
  "main": "build/index.js",