mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
chore(pending): quarantine python Notion+Linear MCP server (Lean-first unification)
This commit is contained in:
parent
f31752bbcc
commit
f5d683242e
1 changed files with 0 additions and 762 deletions
|
|
@ -1,762 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
MCP Server for Notion + Linear Integration
|
||||
|
||||
Provides unified access to Notion and Linear APIs using existing Research Stack surfaces:
|
||||
- Notion: Research database management, page creation, content sync
|
||||
- Linear: Issue tracking, project management, workflow automation
|
||||
- ENE: Credential management via distributed node encryption
|
||||
- Web Surface: HTTP API calls via SwarmWebSurface
|
||||
|
||||
Per AGENTS.md: This is a shim layer. All logic resides in Lean modules.
|
||||
API credentials managed via ENE (AES-256-GCM encrypted).
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import asyncio
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "4-Infrastructure"))
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "0-Core-Formalism"))
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "4-Infrastructure" / "infra"))
|
||||
|
||||
try:
|
||||
from mcp.server import Server
|
||||
from mcp.server.stdio import stdio_server
|
||||
from mcp.types import Tool, TextContent
|
||||
except ImportError:
|
||||
print("MCP SDK not installed. Install with: pip install mcp", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
from infra.web_interaction_surface import SwarmWebInterface, DutyType
|
||||
except ImportError:
|
||||
print("Web interaction surface not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
from infra.ene_cloud_credential_manager import (
|
||||
ENECloudCredentialManager,
|
||||
ENETopologicalStorage,
|
||||
ENENodeBalancer
|
||||
)
|
||||
except ImportError:
|
||||
print("ENE cloud credential manager not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Credential Management via ENE
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@dataclass
|
||||
class APICredentials:
|
||||
"""API credentials managed via ENE."""
|
||||
service: str
|
||||
api_key: str
|
||||
additional_params: Dict[str, str] = None
|
||||
node_id: str = None # Which node provided these credentials
|
||||
|
||||
def __post_init__(self):
|
||||
if self.additional_params is None:
|
||||
self.additional_params = {}
|
||||
|
||||
class CredentialManager:
|
||||
"""
|
||||
Credential manager using ENE for encrypted storage.
|
||||
|
||||
Integrates with:
|
||||
- ENE distributed node system (6-node mesh)
|
||||
- Shamir-secret sharing (6 shards, 2/3 threshold)
|
||||
- AES-256-GCM encryption via ENESecurityManager
|
||||
- Health-weighted node routing via ENENodeBalancer
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.ene_credential_manager: Optional[ENECloudCredentialManager] = None
|
||||
self.credentials: Dict[str, APICredentials] = {}
|
||||
self._init_ene()
|
||||
self._load_from_env_fallback()
|
||||
|
||||
def _init_ene(self):
|
||||
"""Initialize ENE credential manager."""
|
||||
try:
|
||||
self.ene_credential_manager = ENECloudCredentialManager()
|
||||
print("ENE credential manager initialized", file=sys.stderr)
|
||||
except Exception as e:
|
||||
print(f"ENE initialization failed (using fallback): {e}", file=sys.stderr)
|
||||
self.ene_credential_manager = None
|
||||
|
||||
def _load_from_env_fallback(self):
|
||||
"""Load credentials from environment if ENE not available."""
|
||||
if self.ene_credential_manager:
|
||||
# Try to load from ENE first
|
||||
self._load_from_ene()
|
||||
return
|
||||
|
||||
notion_key = os.environ.get("NOTION_API_KEY")
|
||||
notion_db = os.environ.get("NOTION_DATABASE_ID")
|
||||
linear_key = os.environ.get("LINEAR_API_KEY")
|
||||
|
||||
if notion_key:
|
||||
self.credentials["notion"] = APICredentials(
|
||||
service="notion",
|
||||
api_key=notion_key,
|
||||
additional_params={"database_id": notion_db} if notion_db else {}
|
||||
)
|
||||
|
||||
if linear_key:
|
||||
self.credentials["linear"] = APICredentials(
|
||||
service="linear",
|
||||
api_key=linear_key
|
||||
)
|
||||
|
||||
def _load_from_ene(self):
|
||||
"""Load credentials from ENE credential manager."""
|
||||
if not self.ene_credential_manager:
|
||||
return
|
||||
|
||||
# Try to get Notion credentials from ENE
|
||||
try:
|
||||
# Use a default node ID for MCP server
|
||||
node_id = "mcp_server_node"
|
||||
notion_creds = self.ene_credential_manager.get_credential_for_node(
|
||||
node_id, "notion"
|
||||
)
|
||||
if notion_creds:
|
||||
self.credentials["notion"] = APICredentials(
|
||||
service="notion",
|
||||
api_key=notion_creds.get("api_key", ""),
|
||||
additional_params={"database_id": notion_creds.get("database_id")},
|
||||
node_id=node_id
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Failed to load Notion from ENE: {e}", file=sys.stderr)
|
||||
|
||||
# Try to get Linear credentials from ENE
|
||||
try:
|
||||
linear_creds = self.ene_credential_manager.get_credential_for_node(
|
||||
node_id, "linear"
|
||||
)
|
||||
if linear_creds:
|
||||
self.credentials["linear"] = APICredentials(
|
||||
service="linear",
|
||||
api_key=linear_creds.get("api_key", ""),
|
||||
node_id=node_id
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Failed to load Linear from ENE: {e}", file=sys.stderr)
|
||||
|
||||
def store_credential_ene(self, service: str, api_key: str,
|
||||
additional_params: Dict = None) -> str:
|
||||
"""Store credential in ENE (encrypted)."""
|
||||
if not self.ene_credential_manager:
|
||||
raise Exception("ENE not available")
|
||||
|
||||
secret = json.dumps(additional_params) if additional_params else ""
|
||||
|
||||
cred_id = self.ene_credential_manager.store_credential(
|
||||
provider=service,
|
||||
api_key=api_key,
|
||||
secret=secret,
|
||||
node_assignments=["mcp_server_node"]
|
||||
)
|
||||
|
||||
# Reload credentials
|
||||
self._load_from_ene()
|
||||
|
||||
return cred_id
|
||||
|
||||
def get_credentials(self, service: str) -> Optional[APICredentials]:
|
||||
"""Get credentials for service."""
|
||||
return self.credentials.get(service)
|
||||
|
||||
def has_credentials(self, service: str) -> bool:
|
||||
"""Check if credentials exist for service."""
|
||||
return service in self.credentials
|
||||
|
||||
def get_ene_status(self) -> Dict[str, Any]:
|
||||
"""Get ENE integration status."""
|
||||
if not self.ene_credential_manager:
|
||||
return {
|
||||
"ene_enabled": False,
|
||||
"mode": "environment_fallback"
|
||||
}
|
||||
|
||||
return {
|
||||
"ene_enabled": True,
|
||||
"mode": "ene_encrypted",
|
||||
"balancer_stats": self.ene_credential_manager.balancer.get_balancer_stats()
|
||||
}
|
||||
|
||||
# Global credential manager
|
||||
_credential_manager: Optional[CredentialManager] = None
|
||||
|
||||
def get_credential_manager() -> CredentialManager:
|
||||
"""Get or create credential manager."""
|
||||
global _credential_manager
|
||||
if _credential_manager is None:
|
||||
_credential_manager = CredentialManager()
|
||||
return _credential_manager
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Notion API Client
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
class NotionClient:
|
||||
"""Notion API client using web interaction surface."""
|
||||
|
||||
BASE_URL = "https://api.notion.com/v1"
|
||||
|
||||
def __init__(self, web_interface: SwarmWebInterface, credentials: APICredentials):
|
||||
self.web = web_interface
|
||||
self.credentials = credentials
|
||||
self.headers = {
|
||||
"Authorization": f"Bearer {credentials.api_key}",
|
||||
"Notion-Version": "2022-06-28",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
async def query_database(self, database_id: str = None,
|
||||
filter_params: Dict = None) -> Dict[str, Any]:
|
||||
"""Query Notion database."""
|
||||
db_id = database_id or self.credentials.additional_params.get("database_id")
|
||||
if not db_id:
|
||||
return {"error": "No database ID provided"}
|
||||
|
||||
url = f"{self.BASE_URL}/databases/{db_id}/query"
|
||||
payload = {"filter": filter_params} if filter_params else {}
|
||||
|
||||
# Use web surface for API call
|
||||
task = self.web.surface.submit_task(
|
||||
DutyType.CONTENT_EXTRACTION,
|
||||
url,
|
||||
priority=8,
|
||||
options={
|
||||
"method": "POST",
|
||||
"headers": self.headers,
|
||||
"body": json.dumps(payload)
|
||||
}
|
||||
)
|
||||
|
||||
result = self.web.surface.execute_task(task["task_id"])
|
||||
return result
|
||||
|
||||
async def create_page(self, parent_id: str, properties: Dict,
|
||||
content: List[Dict] = None) -> Dict[str, Any]:
|
||||
"""Create page in Notion."""
|
||||
url = f"{self.BASE_URL}/pages"
|
||||
|
||||
page_data = {
|
||||
"parent": {"database_id": parent_id} if parent_id.startswith("-") else {"page_id": parent_id},
|
||||
"properties": properties
|
||||
}
|
||||
|
||||
if content:
|
||||
page_data["children"] = content
|
||||
|
||||
task = self.web.surface.submit_task(
|
||||
DutyType.FORM_INTERACTION,
|
||||
url,
|
||||
priority=8,
|
||||
options={
|
||||
"method": "POST",
|
||||
"headers": self.headers,
|
||||
"body": json.dumps(page_data)
|
||||
}
|
||||
)
|
||||
|
||||
result = self.web.surface.execute_task(task["task_id"])
|
||||
return result
|
||||
|
||||
async def update_page(self, page_id: str, properties: Dict) -> Dict[str, Any]:
|
||||
"""Update page in Notion."""
|
||||
url = f"{self.BASE_URL}/pages/{page_id}"
|
||||
|
||||
task = self.web.surface.submit_task(
|
||||
DutyType.FORM_INTERACTION,
|
||||
url,
|
||||
priority=7,
|
||||
options={
|
||||
"method": "PATCH",
|
||||
"headers": self.headers,
|
||||
"body": json.dumps({"properties": properties})
|
||||
}
|
||||
)
|
||||
|
||||
result = self.web.surface.execute_task(task["task_id"])
|
||||
return result
|
||||
|
||||
async def get_page(self, page_id: str) -> Dict[str, Any]:
|
||||
"""Get page content."""
|
||||
url = f"{self.BASE_URL}/pages/{page_id}"
|
||||
|
||||
task = self.web.surface.submit_task(
|
||||
DutyType.CONTENT_EXTRACTION,
|
||||
url,
|
||||
priority=7,
|
||||
options={
|
||||
"method": "GET",
|
||||
"headers": self.headers
|
||||
}
|
||||
)
|
||||
|
||||
result = self.web.surface.execute_task(task["task_id"])
|
||||
return result
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Linear API Client
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
class LinearClient:
|
||||
"""Linear API client using web interaction surface."""
|
||||
|
||||
BASE_URL = "https://api.linear.app/graphql"
|
||||
|
||||
def __init__(self, web_interface: SwarmWebInterface, credentials: APICredentials):
|
||||
self.web = web_interface
|
||||
self.credentials = credentials
|
||||
self.headers = {
|
||||
"Authorization": f"{credentials.api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
async def query_issues(self, team_key: str = None,
|
||||
status: str = None) -> Dict[str, Any]:
|
||||
"""Query Linear issues."""
|
||||
query = """
|
||||
query {
|
||||
issues(first: 50) {
|
||||
nodes {
|
||||
id
|
||||
title
|
||||
description
|
||||
state {
|
||||
name
|
||||
}
|
||||
priority
|
||||
assignee {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
task = self.web.surface.submit_task(
|
||||
DutyType.CONTENT_EXTRACTION,
|
||||
self.BASE_URL,
|
||||
priority=8,
|
||||
options={
|
||||
"method": "POST",
|
||||
"headers": self.headers,
|
||||
"body": json.dumps({"query": query})
|
||||
}
|
||||
)
|
||||
|
||||
result = self.web.surface.execute_task(task["task_id"])
|
||||
return result
|
||||
|
||||
async def create_issue(self, team_id: str, title: str,
|
||||
description: str = None) -> Dict[str, Any]:
|
||||
"""Create issue in Linear."""
|
||||
mutation = """
|
||||
mutation($input: IssueCreateInput!) {
|
||||
issueCreate(input: $input) {
|
||||
issue {
|
||||
id
|
||||
title
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
variables = {
|
||||
"input": {
|
||||
"teamId": team_id,
|
||||
"title": title,
|
||||
"description": description or ""
|
||||
}
|
||||
}
|
||||
|
||||
task = self.web.surface.submit_task(
|
||||
DutyType.FORM_INTERACTION,
|
||||
self.BASE_URL,
|
||||
priority=8,
|
||||
options={
|
||||
"method": "POST",
|
||||
"headers": self.headers,
|
||||
"body": json.dumps({"query": mutation, "variables": variables})
|
||||
}
|
||||
)
|
||||
|
||||
result = self.web.surface.execute_task(task["task_id"])
|
||||
return result
|
||||
|
||||
async def update_issue_state(self, issue_id: str,
|
||||
state_id: str) -> Dict[str, Any]:
|
||||
"""Update issue state."""
|
||||
mutation = """
|
||||
mutation($input: IssueUpdateInput!) {
|
||||
issueUpdate(input: $input) {
|
||||
issue {
|
||||
id
|
||||
state {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
variables = {
|
||||
"input": {
|
||||
"id": issue_id,
|
||||
"stateId": state_id
|
||||
}
|
||||
}
|
||||
|
||||
task = self.web.surface.submit_task(
|
||||
DutyType.FORM_INTERACTION,
|
||||
self.BASE_URL,
|
||||
priority=7,
|
||||
options={
|
||||
"method": "POST",
|
||||
"headers": self.headers,
|
||||
"body": json.dumps({"query": mutation, "variables": variables})
|
||||
}
|
||||
)
|
||||
|
||||
result = self.web.surface.execute_task(task["task_id"])
|
||||
return result
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# MCP Server
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# Global instances
|
||||
_web_interface: Optional[SwarmWebInterface] = None
|
||||
_notion_client: Optional[NotionClient] = None
|
||||
_linear_client: Optional[LinearClient] = None
|
||||
|
||||
def get_web_interface() -> SwarmWebInterface:
|
||||
"""Get or create web interface."""
|
||||
global _web_interface
|
||||
if _web_interface is None:
|
||||
_web_interface = SwarmWebInterface()
|
||||
return _web_interface
|
||||
|
||||
def get_notion_client() -> Optional[NotionClient]:
|
||||
"""Get or create Notion client."""
|
||||
global _notion_client
|
||||
if _notion_client is None:
|
||||
creds = get_credential_manager().get_credentials("notion")
|
||||
if creds:
|
||||
_notion_client = NotionClient(get_web_interface(), creds)
|
||||
return _notion_client
|
||||
|
||||
def get_linear_client() -> Optional[LinearClient]:
|
||||
"""Get or create Linear client."""
|
||||
global _linear_client
|
||||
if _linear_client is None:
|
||||
creds = get_credential_manager().get_credentials("linear")
|
||||
if creds:
|
||||
_linear_client = LinearClient(get_web_interface(), creds)
|
||||
return _linear_client
|
||||
|
||||
# Create MCP server
|
||||
server = Server("notion-linear-integration")
|
||||
|
||||
@server.list_tools()
|
||||
async def list_tools() -> List[Tool]:
|
||||
"""List available Notion + Linear tools."""
|
||||
return [
|
||||
# Notion Tools
|
||||
Tool(
|
||||
name="notion_query_database",
|
||||
description="Query Notion database with optional filters.",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"database_id": {"type": "string", "description": "Notion database ID (32 chars)"},
|
||||
"filter": {"type": "object", "description": "Notion filter object"}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="notion_create_page",
|
||||
description="Create a new page in Notion.",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"parent_id": {"type": "string", "description": "Parent database or page ID"},
|
||||
"properties": {"type": "object", "description": "Page properties"},
|
||||
"content": {"type": "array", "description": "Page content blocks", "items": {"type": "object"}}
|
||||
},
|
||||
"required": ["parent_id", "properties"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="notion_update_page",
|
||||
description="Update an existing page in Notion.",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"page_id": {"type": "string", "description": "Page ID to update"},
|
||||
"properties": {"type": "object", "description": "Properties to update"}
|
||||
},
|
||||
"required": ["page_id", "properties"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="notion_get_page",
|
||||
description="Get page content from Notion.",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"page_id": {"type": "string", "description": "Page ID"}
|
||||
},
|
||||
"required": ["page_id"]
|
||||
}
|
||||
),
|
||||
# Linear Tools
|
||||
Tool(
|
||||
name="linear_query_issues",
|
||||
description="Query issues from Linear.",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"team_key": {"type": "string", "description": "Team key filter"},
|
||||
"status": {"type": "string", "description": "Status filter"}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="linear_create_issue",
|
||||
description="Create a new issue in Linear.",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"team_id": {"type": "string", "description": "Team ID"},
|
||||
"title": {"type": "string", "description": "Issue title"},
|
||||
"description": {"type": "string", "description": "Issue description"}
|
||||
},
|
||||
"required": ["team_id", "title"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="linear_update_issue_state",
|
||||
description="Update issue state in Linear.",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"issue_id": {"type": "string", "description": "Issue ID"},
|
||||
"state_id": {"type": "string", "description": "State ID"}
|
||||
},
|
||||
"required": ["issue_id", "state_id"]
|
||||
}
|
||||
),
|
||||
# System Tools
|
||||
Tool(
|
||||
name="check_credentials",
|
||||
description="Check which API credentials are configured.",
|
||||
inputSchema={"type": "object", "properties": {}}
|
||||
),
|
||||
Tool(
|
||||
name="store_credential_ene",
|
||||
description="Store API credential in ENE (encrypted).",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"service": {"type": "string", "description": "Service name (notion, linear)"},
|
||||
"api_key": {"type": "string", "description": "API key"},
|
||||
"additional_params": {"type": "object", "description": "Additional parameters"}
|
||||
},
|
||||
"required": ["service", "api_key"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="get_ene_status",
|
||||
description="Get ENE integration status and node health.",
|
||||
inputSchema={"type": "object", "properties": {}}
|
||||
),
|
||||
Tool(
|
||||
name="sync_research_to_notion",
|
||||
description="Sync Research Stack papers to Notion database.",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"paper_path": {"type": "string", "description": "Path to paper markdown file"}
|
||||
},
|
||||
"required": ["paper_path"]
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
@server.call_tool()
|
||||
async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
|
||||
"""Execute tool logic."""
|
||||
|
||||
if name == "check_credentials":
|
||||
mgr = get_credential_manager()
|
||||
status = {
|
||||
"notion": mgr.has_credentials("notion"),
|
||||
"linear": mgr.has_credentials("linear"),
|
||||
"ene_status": mgr.get_ene_status()
|
||||
}
|
||||
return [TextContent(type="text", text=json.dumps(status, indent=2))]
|
||||
|
||||
elif name == "store_credential_ene":
|
||||
mgr = get_credential_manager()
|
||||
try:
|
||||
cred_id = mgr.store_credential_ene(
|
||||
service=arguments["service"],
|
||||
api_key=arguments["api_key"],
|
||||
additional_params=arguments.get("additional_params")
|
||||
)
|
||||
return [TextContent(type="text", text=json.dumps({
|
||||
"success": True,
|
||||
"credential_id": cred_id,
|
||||
"encrypted": True,
|
||||
"storage": "ENE (AES-256-GCM)"
|
||||
}, indent=2))]
|
||||
except Exception as e:
|
||||
return [TextContent(type="text", text=json.dumps({
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}, indent=2))]
|
||||
|
||||
elif name == "get_ene_status":
|
||||
mgr = get_credential_manager()
|
||||
status = mgr.get_ene_status()
|
||||
return [TextContent(type="text", text=json.dumps(status, indent=2))]
|
||||
|
||||
# Notion tools
|
||||
elif name.startswith("notion_"):
|
||||
client = get_notion_client()
|
||||
if not client:
|
||||
return [TextContent(type="text", text="Error: Notion credentials not configured")]
|
||||
|
||||
if name == "notion_query_database":
|
||||
result = await client.query_database(
|
||||
database_id=arguments.get("database_id"),
|
||||
filter_params=arguments.get("filter")
|
||||
)
|
||||
return [TextContent(type="text", text=json.dumps(result, indent=2))]
|
||||
|
||||
elif name == "notion_create_page":
|
||||
result = await client.create_page(
|
||||
parent_id=arguments["parent_id"],
|
||||
properties=arguments["properties"],
|
||||
content=arguments.get("content")
|
||||
)
|
||||
return [TextContent(type="text", text=json.dumps(result, indent=2))]
|
||||
|
||||
elif name == "notion_update_page":
|
||||
result = await client.update_page(
|
||||
page_id=arguments["page_id"],
|
||||
properties=arguments["properties"]
|
||||
)
|
||||
return [TextContent(type="text", text=json.dumps(result, indent=2))]
|
||||
|
||||
elif name == "notion_get_page":
|
||||
result = await client.get_page(page_id=arguments["page_id"])
|
||||
return [TextContent(type="text", text=json.dumps(result, indent=2))]
|
||||
|
||||
# Linear tools
|
||||
elif name.startswith("linear_"):
|
||||
client = get_linear_client()
|
||||
if not client:
|
||||
return [TextContent(type="text", text="Error: Linear credentials not configured")]
|
||||
|
||||
if name == "linear_query_issues":
|
||||
result = await client.query_issues(
|
||||
team_key=arguments.get("team_key"),
|
||||
status=arguments.get("status")
|
||||
)
|
||||
return [TextContent(type="text", text=json.dumps(result, indent=2))]
|
||||
|
||||
elif name == "linear_create_issue":
|
||||
result = await client.create_issue(
|
||||
team_id=arguments["team_id"],
|
||||
title=arguments["title"],
|
||||
description=arguments.get("description")
|
||||
)
|
||||
return [TextContent(type="text", text=json.dumps(result, indent=2))]
|
||||
|
||||
elif name == "linear_update_issue_state":
|
||||
result = await client.update_issue_state(
|
||||
issue_id=arguments["issue_id"],
|
||||
state_id=arguments["state_id"]
|
||||
)
|
||||
return [TextContent(type="text", text=json.dumps(result, indent=2))]
|
||||
|
||||
# Sync tool
|
||||
elif name == "sync_research_to_notion":
|
||||
client = get_notion_client()
|
||||
if not client:
|
||||
return [TextContent(type="text", text="Error: Notion credentials not configured")]
|
||||
|
||||
paper_path = arguments["paper_path"]
|
||||
paper_file = Path(paper_path)
|
||||
|
||||
if not paper_file.exists():
|
||||
return [TextContent(type="text", text=f"Error: Paper not found at {paper_path}")]
|
||||
|
||||
# Read paper content
|
||||
with open(paper_file) as f:
|
||||
content = f.read()
|
||||
|
||||
# Extract title (first line with #)
|
||||
title = "Untitled Paper"
|
||||
for line in content.split("\n"):
|
||||
if line.startswith("# "):
|
||||
title = line[2:].strip()
|
||||
break
|
||||
|
||||
# Create page in Notion
|
||||
db_id = client.credentials.additional_params.get("database_id")
|
||||
if not db_id:
|
||||
return [TextContent(type="text", text="Error: No Notion database ID configured")]
|
||||
|
||||
properties = {
|
||||
"title": {
|
||||
"title": [{"text": {"content": title}}]
|
||||
},
|
||||
"Status": {
|
||||
"select": {"name": "In Progress"}
|
||||
}
|
||||
}
|
||||
|
||||
result = await client.create_page(
|
||||
parent_id=db_id,
|
||||
properties=properties,
|
||||
content=[{
|
||||
"object": "block",
|
||||
"type": "paragraph",
|
||||
"paragraph": {
|
||||
"rich_text": [{"type": "text", "text": {"content": content[:2000]}}]
|
||||
}
|
||||
}]
|
||||
)
|
||||
|
||||
return [TextContent(type="text", text=json.dumps(result, indent=2))]
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unknown tool: {name}")
|
||||
|
||||
async def main():
|
||||
async with stdio_server() as (read_stream, write_stream):
|
||||
await server.run(read_stream, write_stream, server.create_initialization_options())
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Loading…
Add table
Reference in a new issue