mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-03 06:42:27 +08:00
Compare commits
6 Commits
feature/mc
...
v0.4.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9415d24e7 | ||
|
|
439bdd4577 | ||
|
|
98b890bb06 | ||
|
|
f039e4a3c8 | ||
|
|
7857858074 | ||
|
|
f0919117eb |
7
.github/workflows/docker-build.yml
vendored
7
.github/workflows/docker-build.yml
vendored
@@ -80,8 +80,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Push to ECR (triggers App Runner auto-deploy)
|
- name: Push to ECR (triggers App Runner auto-deploy)
|
||||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||||
|
env:
|
||||||
|
REPO_LOWER: ${{ github.repository }}
|
||||||
run: |
|
run: |
|
||||||
docker pull ghcr.io/${{ github.repository }}:latest
|
REPO_LOWER=$(echo "$REPO_LOWER" | tr '[:upper:]' '[:lower:]')
|
||||||
docker tag ghcr.io/${{ github.repository }}:latest ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-1.amazonaws.com/next-ai-draw-io:latest
|
docker pull ghcr.io/${REPO_LOWER}:latest
|
||||||
|
docker tag ghcr.io/${REPO_LOWER}:latest ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-1.amazonaws.com/next-ai-draw-io:latest
|
||||||
docker push ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-1.amazonaws.com/next-ai-draw-io:latest
|
docker push ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-1.amazonaws.com/next-ai-draw-io:latest
|
||||||
|
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
/node_modules
|
||||||
|
packages/*/node_modules
|
||||||
|
packages/*/dist
|
||||||
/.pnp
|
/.pnp
|
||||||
.pnp.*
|
.pnp.*
|
||||||
.yarn/*
|
.yarn/*
|
||||||
@@ -47,3 +49,4 @@ push-via-ec2.sh
|
|||||||
.open-next/
|
.open-next/
|
||||||
.wrangler/
|
.wrangler/
|
||||||
.env*.local
|
.env*.local
|
||||||
|
|
||||||
|
|||||||
31
README.md
31
README.md
@@ -30,6 +30,7 @@ https://github.com/user-attachments/assets/9d60a3e8-4a1c-4b5e-acbb-26af2d3eabd1
|
|||||||
- [Table of Contents](#table-of-contents)
|
- [Table of Contents](#table-of-contents)
|
||||||
- [Examples](#examples)
|
- [Examples](#examples)
|
||||||
- [Features](#features)
|
- [Features](#features)
|
||||||
|
- [MCP Server (Preview)](#mcp-server-preview)
|
||||||
- [Getting Started](#getting-started)
|
- [Getting Started](#getting-started)
|
||||||
- [Try it Online](#try-it-online)
|
- [Try it Online](#try-it-online)
|
||||||
- [Run with Docker (Recommended)](#run-with-docker-recommended)
|
- [Run with Docker (Recommended)](#run-with-docker-recommended)
|
||||||
@@ -92,6 +93,36 @@ Here are some example prompts and their generated diagrams:
|
|||||||
- **Cloud Architecture Diagram Support**: Specialized support for generating cloud architecture diagrams (AWS, GCP, Azure)
|
- **Cloud Architecture Diagram Support**: Specialized support for generating cloud architecture diagrams (AWS, GCP, Azure)
|
||||||
- **Animated Connectors**: Create dynamic and animated connectors between diagram elements for better visualization
|
- **Animated Connectors**: Create dynamic and animated connectors between diagram elements for better visualization
|
||||||
|
|
||||||
|
## MCP Server (Preview)
|
||||||
|
|
||||||
|
> **Preview Feature**: This feature is experimental and may not stable.
|
||||||
|
|
||||||
|
Use Next AI Draw.io with AI agents like Claude Desktop, Cursor, and VS Code via MCP (Model Context Protocol).
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"drawio": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["@next-ai-drawio/mcp-server@latest"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Claude Code CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude mcp add drawio -- npx @next-ai-drawio/mcp-server@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Then ask Claude to create diagrams:
|
||||||
|
> "Create a flowchart showing user authentication with login, MFA, and session management"
|
||||||
|
|
||||||
|
The diagram appears in your browser in real-time!
|
||||||
|
|
||||||
|
See the [MCP Server README](./packages/mcp-server/README.md) for VS Code, Cursor, and other client configurations.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
### Try it Online
|
### Try it Online
|
||||||
|
|||||||
27
app/manifest.ts
Normal file
27
app/manifest.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import type { MetadataRoute } from "next";
|
||||||
|
|
||||||
|
export default function manifest(): MetadataRoute.Manifest {
|
||||||
|
return {
|
||||||
|
name: 'Next AI Draw.io',
|
||||||
|
short_name: 'AIDraw.io',
|
||||||
|
description: 'Create AWS architecture diagrams, flowcharts, and technical diagrams using AI. Free online tool integrating draw.io with AI assistance for professional diagram creation.',
|
||||||
|
start_url: '/',
|
||||||
|
display: 'standalone',
|
||||||
|
background_color: '#f9fafb',
|
||||||
|
theme_color: '#171d26',
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: '/favicon-192x192.png',
|
||||||
|
sizes: '192x192',
|
||||||
|
type: 'image/png',
|
||||||
|
purpose: 'any',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/favicon-512x512.png',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png',
|
||||||
|
purpose: 'any',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
33
app/page.tsx
33
app/page.tsx
@@ -15,8 +15,13 @@ const drawioBaseUrl =
|
|||||||
process.env.NEXT_PUBLIC_DRAWIO_BASE_URL || "https://embed.diagrams.net"
|
process.env.NEXT_PUBLIC_DRAWIO_BASE_URL || "https://embed.diagrams.net"
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { drawioRef, handleDiagramExport, onDrawioLoad, resetDrawioReady } =
|
const {
|
||||||
useDiagram()
|
drawioRef,
|
||||||
|
handleDiagramExport,
|
||||||
|
onDrawioLoad,
|
||||||
|
resetDrawioReady,
|
||||||
|
saveDiagramToStorage,
|
||||||
|
} = useDiagram()
|
||||||
const [isMobile, setIsMobile] = useState(false)
|
const [isMobile, setIsMobile] = useState(false)
|
||||||
const [isChatVisible, setIsChatVisible] = useState(true)
|
const [isChatVisible, setIsChatVisible] = useState(true)
|
||||||
const [drawioUi, setDrawioUi] = useState<"min" | "sketch">("min")
|
const [drawioUi, setDrawioUi] = useState<"min" | "sketch">("min")
|
||||||
@@ -35,12 +40,10 @@ export default function Home() {
|
|||||||
|
|
||||||
const savedDarkMode = localStorage.getItem("next-ai-draw-io-dark-mode")
|
const savedDarkMode = localStorage.getItem("next-ai-draw-io-dark-mode")
|
||||||
if (savedDarkMode !== null) {
|
if (savedDarkMode !== null) {
|
||||||
// Use saved preference
|
|
||||||
const isDark = savedDarkMode === "true"
|
const isDark = savedDarkMode === "true"
|
||||||
setDarkMode(isDark)
|
setDarkMode(isDark)
|
||||||
document.documentElement.classList.toggle("dark", isDark)
|
document.documentElement.classList.toggle("dark", isDark)
|
||||||
} else {
|
} else {
|
||||||
// First visit: match browser preference
|
|
||||||
const prefersDark = window.matchMedia(
|
const prefersDark = window.matchMedia(
|
||||||
"(prefers-color-scheme: dark)",
|
"(prefers-color-scheme: dark)",
|
||||||
).matches
|
).matches
|
||||||
@@ -58,12 +61,20 @@ export default function Home() {
|
|||||||
setIsLoaded(true)
|
setIsLoaded(true)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const toggleDarkMode = () => {
|
const handleDarkModeChange = async () => {
|
||||||
|
await saveDiagramToStorage()
|
||||||
const newValue = !darkMode
|
const newValue = !darkMode
|
||||||
setDarkMode(newValue)
|
setDarkMode(newValue)
|
||||||
localStorage.setItem("next-ai-draw-io-dark-mode", String(newValue))
|
localStorage.setItem("next-ai-draw-io-dark-mode", String(newValue))
|
||||||
document.documentElement.classList.toggle("dark", newValue)
|
document.documentElement.classList.toggle("dark", newValue)
|
||||||
// Reset so onDrawioLoad fires again after remount
|
resetDrawioReady()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDrawioUiChange = async () => {
|
||||||
|
await saveDiagramToStorage()
|
||||||
|
const newUi = drawioUi === "min" ? "sketch" : "min"
|
||||||
|
localStorage.setItem("drawio-theme", newUi)
|
||||||
|
setDrawioUi(newUi)
|
||||||
resetDrawioReady()
|
resetDrawioReady()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,15 +193,9 @@ export default function Home() {
|
|||||||
isVisible={isChatVisible}
|
isVisible={isChatVisible}
|
||||||
onToggleVisibility={toggleChatPanel}
|
onToggleVisibility={toggleChatPanel}
|
||||||
drawioUi={drawioUi}
|
drawioUi={drawioUi}
|
||||||
onToggleDrawioUi={() => {
|
onToggleDrawioUi={handleDrawioUiChange}
|
||||||
const newUi =
|
|
||||||
drawioUi === "min" ? "sketch" : "min"
|
|
||||||
localStorage.setItem("drawio-theme", newUi)
|
|
||||||
setDrawioUi(newUi)
|
|
||||||
resetDrawioReady()
|
|
||||||
}}
|
|
||||||
darkMode={darkMode}
|
darkMode={darkMode}
|
||||||
onToggleDarkMode={toggleDarkMode}
|
onToggleDarkMode={handleDarkModeChange}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
onCloseProtectionChange={setCloseProtection}
|
onCloseProtectionChange={setCloseProtection}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Cloud, FileText, GitBranch, Palette, Zap } from "lucide-react"
|
import {
|
||||||
|
Cloud,
|
||||||
|
FileText,
|
||||||
|
GitBranch,
|
||||||
|
Palette,
|
||||||
|
Terminal,
|
||||||
|
Zap,
|
||||||
|
} from "lucide-react"
|
||||||
|
|
||||||
interface ExampleCardProps {
|
interface ExampleCardProps {
|
||||||
icon: React.ReactNode
|
icon: React.ReactNode
|
||||||
@@ -108,6 +115,33 @@ export default function ExamplePanel({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="py-6 px-2 animate-fade-in">
|
<div className="py-6 px-2 animate-fade-in">
|
||||||
|
{/* MCP Server Notice */}
|
||||||
|
<a
|
||||||
|
href="https://github.com/DayuanJiang/next-ai-draw-io/tree/main/packages/mcp-server"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="block mb-4 p-3 rounded-xl bg-gradient-to-r from-purple-500/10 to-blue-500/10 border border-purple-500/20 hover:border-purple-500/40 transition-colors group"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-purple-500/20 flex items-center justify-center shrink-0">
|
||||||
|
<Terminal className="w-4 h-4 text-purple-500" />
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm font-medium text-foreground group-hover:text-purple-500 transition-colors">
|
||||||
|
MCP Server
|
||||||
|
</span>
|
||||||
|
<span className="px-1.5 py-0.5 text-[10px] font-semibold bg-purple-500 text-white rounded">
|
||||||
|
PREVIEW
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Use in Claude Desktop, VS Code & Cursor
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
{/* Welcome section */}
|
{/* Welcome section */}
|
||||||
<div className="text-center mb-6">
|
<div className="text-center mb-6">
|
||||||
<h2 className="text-lg font-semibold text-foreground mb-2">
|
<h2 className="text-lg font-semibold text-foreground mb-2">
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ interface DiagramContextType {
|
|||||||
format: ExportFormat,
|
format: ExportFormat,
|
||||||
sessionId?: string,
|
sessionId?: string,
|
||||||
) => void
|
) => void
|
||||||
|
saveDiagramToStorage: () => Promise<void>
|
||||||
isDrawioReady: boolean
|
isDrawioReady: boolean
|
||||||
onDrawioLoad: () => void
|
onDrawioLoad: () => void
|
||||||
resetDrawioReady: () => void
|
resetDrawioReady: () => void
|
||||||
@@ -82,6 +83,30 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save current diagram to localStorage (used before theme/UI changes)
|
||||||
|
const saveDiagramToStorage = async (): Promise<void> => {
|
||||||
|
if (!drawioRef.current) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const currentXml = await Promise.race([
|
||||||
|
new Promise<string>((resolve) => {
|
||||||
|
resolverRef.current = resolve
|
||||||
|
drawioRef.current?.exportDiagram({ format: "xmlsvg" })
|
||||||
|
}),
|
||||||
|
new Promise<string>((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error("Export timeout")), 2000),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
// Only save if diagram has meaningful content (not empty template)
|
||||||
|
if (currentXml && currentXml.length > 300) {
|
||||||
|
localStorage.setItem(STORAGE_DIAGRAM_XML_KEY, currentXml)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to save diagram to storage:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const loadDiagram = (
|
const loadDiagram = (
|
||||||
chart: string,
|
chart: string,
|
||||||
skipValidation?: boolean,
|
skipValidation?: boolean,
|
||||||
@@ -280,6 +305,7 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
|||||||
handleDiagramExport,
|
handleDiagramExport,
|
||||||
clearDiagram,
|
clearDiagram,
|
||||||
saveDiagramToFile,
|
saveDiagramToFile,
|
||||||
|
saveDiagramToStorage,
|
||||||
isDrawioReady,
|
isDrawioReady,
|
||||||
onDrawioLoad,
|
onDrawioLoad,
|
||||||
resetDrawioReady,
|
resetDrawioReady,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
|||||||
- [目录](#目录)
|
- [目录](#目录)
|
||||||
- [示例](#示例)
|
- [示例](#示例)
|
||||||
- [功能特性](#功能特性)
|
- [功能特性](#功能特性)
|
||||||
|
- [MCP服务器(预览)](#mcp服务器预览)
|
||||||
- [快速开始](#快速开始)
|
- [快速开始](#快速开始)
|
||||||
- [在线试用](#在线试用)
|
- [在线试用](#在线试用)
|
||||||
- [使用Docker运行(推荐)](#使用docker运行推荐)
|
- [使用Docker运行(推荐)](#使用docker运行推荐)
|
||||||
@@ -88,6 +89,36 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
|||||||
- **云架构图支持**:专门支持生成云架构图(AWS、GCP、Azure)
|
- **云架构图支持**:专门支持生成云架构图(AWS、GCP、Azure)
|
||||||
- **动画连接器**:在图表元素之间创建动态动画连接器,实现更好的可视化效果
|
- **动画连接器**:在图表元素之间创建动态动画连接器,实现更好的可视化效果
|
||||||
|
|
||||||
|
## MCP服务器(预览)
|
||||||
|
|
||||||
|
> **预览功能**:此功能为实验性功能,可能会有变化。
|
||||||
|
|
||||||
|
通过MCP(模型上下文协议)在Claude Desktop、Cursor和VS Code等AI代理中使用Next AI Draw.io。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"drawio": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["@next-ai-drawio/mcp-server@latest"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Claude Code CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude mcp add drawio -- npx @next-ai-drawio/mcp-server@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
然后让Claude创建图表:
|
||||||
|
> "创建一个展示用户认证流程的流程图,包含登录、MFA和会话管理"
|
||||||
|
|
||||||
|
图表会实时显示在浏览器中!
|
||||||
|
|
||||||
|
详情请参阅[MCP服务器README](../packages/mcp-server/README.md),了解VS Code、Cursor等客户端配置。
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
### 在线试用
|
### 在线试用
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
|||||||
- [目次](#目次)
|
- [目次](#目次)
|
||||||
- [例](#例)
|
- [例](#例)
|
||||||
- [機能](#機能)
|
- [機能](#機能)
|
||||||
|
- [MCPサーバー(プレビュー)](#mcpサーバープレビュー)
|
||||||
- [はじめに](#はじめに)
|
- [はじめに](#はじめに)
|
||||||
- [オンラインで試す](#オンラインで試す)
|
- [オンラインで試す](#オンラインで試す)
|
||||||
- [Dockerで実行(推奨)](#dockerで実行推奨)
|
- [Dockerで実行(推奨)](#dockerで実行推奨)
|
||||||
@@ -88,6 +89,36 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
|||||||
- **クラウドアーキテクチャダイアグラムサポート**:クラウドアーキテクチャダイアグラムの生成を専門的にサポート(AWS、GCP、Azure)
|
- **クラウドアーキテクチャダイアグラムサポート**:クラウドアーキテクチャダイアグラムの生成を専門的にサポート(AWS、GCP、Azure)
|
||||||
- **アニメーションコネクタ**:より良い可視化のためにダイアグラム要素間に動的でアニメーション化されたコネクタを作成
|
- **アニメーションコネクタ**:より良い可視化のためにダイアグラム要素間に動的でアニメーション化されたコネクタを作成
|
||||||
|
|
||||||
|
## MCPサーバー(プレビュー)
|
||||||
|
|
||||||
|
> **プレビュー機能**:この機能は実験的であり、変更される可能性があります。
|
||||||
|
|
||||||
|
MCP(Model Context Protocol)を介して、Claude Desktop、Cursor、VS CodeなどのAIエージェントでNext AI Draw.ioを使用できます。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"drawio": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["@next-ai-drawio/mcp-server@latest"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Claude Code CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude mcp add drawio -- npx @next-ai-drawio/mcp-server@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Claudeにダイアグラムの作成を依頼:
|
||||||
|
> 「ログイン、MFA、セッション管理を含むユーザー認証のフローチャートを作成してください」
|
||||||
|
|
||||||
|
ダイアグラムがリアルタイムでブラウザに表示されます!
|
||||||
|
|
||||||
|
詳細は[MCPサーバーREADME](../packages/mcp-server/README.md)をご覧ください(VS Code、Cursorなどのクライアント設定も含む)。
|
||||||
|
|
||||||
## はじめに
|
## はじめに
|
||||||
|
|
||||||
### オンラインで試す
|
### オンラインで試す
|
||||||
|
|||||||
@@ -136,6 +136,23 @@ Optional custom URL:
|
|||||||
OLLAMA_BASE_URL=http://localhost:11434
|
OLLAMA_BASE_URL=http://localhost:11434
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Vercel AI Gateway
|
||||||
|
|
||||||
|
Vercel AI Gateway provides unified access to multiple AI providers through a single API key. This simplifies authentication and allows you to switch between providers without managing multiple API keys.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AI_GATEWAY_API_KEY=your_gateway_api_key
|
||||||
|
AI_MODEL=openai/gpt-4o
|
||||||
|
```
|
||||||
|
|
||||||
|
Model format uses `provider/model` syntax:
|
||||||
|
|
||||||
|
- `openai/gpt-4o` - OpenAI GPT-4o
|
||||||
|
- `anthropic/claude-sonnet-4-5` - Anthropic Claude Sonnet 4.5
|
||||||
|
- `google/gemini-2.0-flash` - Google Gemini 2.0 Flash
|
||||||
|
|
||||||
|
Get your API key from the [Vercel AI Gateway dashboard](https://vercel.com/ai-gateway).
|
||||||
|
|
||||||
## Auto-Detection
|
## Auto-Detection
|
||||||
|
|
||||||
If you only configure **one** provider's API key, the system will automatically detect and use that provider. No need to set `AI_PROVIDER`.
|
If you only configure **one** provider's API key, the system will automatically detect and use that provider. No need to set `AI_PROVIDER`.
|
||||||
@@ -143,7 +160,7 @@ If you only configure **one** provider's API key, the system will automatically
|
|||||||
If you configure **multiple** API keys, you must explicitly set `AI_PROVIDER`:
|
If you configure **multiple** API keys, you must explicitly set `AI_PROVIDER`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
AI_PROVIDER=google # or: openai, anthropic, deepseek, siliconflow, azure, bedrock, openrouter, ollama
|
AI_PROVIDER=google # or: openai, anthropic, deepseek, siliconflow, azure, bedrock, openrouter, ollama, gateway
|
||||||
```
|
```
|
||||||
|
|
||||||
## Model Capability Requirements
|
## Model Capability Requirements
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# AI Provider Configuration
|
# AI Provider Configuration
|
||||||
# AI_PROVIDER: Which provider to use
|
# AI_PROVIDER: Which provider to use
|
||||||
# Options: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow
|
# Options: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow, gateway
|
||||||
# Default: bedrock
|
# Default: bedrock
|
||||||
AI_PROVIDER=bedrock
|
AI_PROVIDER=bedrock
|
||||||
|
|
||||||
@@ -68,6 +68,11 @@ AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0
|
|||||||
# SILICONFLOW_API_KEY=sk-...
|
# SILICONFLOW_API_KEY=sk-...
|
||||||
# SILICONFLOW_BASE_URL=https://api.siliconflow.com/v1 # Optional: switch to https://api.siliconflow.cn/v1 if needed
|
# SILICONFLOW_BASE_URL=https://api.siliconflow.com/v1 # Optional: switch to https://api.siliconflow.cn/v1 if needed
|
||||||
|
|
||||||
|
# Vercel AI Gateway Configuration
|
||||||
|
# Get your API key from: https://vercel.com/ai-gateway
|
||||||
|
# Model format: "provider/model" e.g., "openai/gpt-4o", "anthropic/claude-sonnet-4-5"
|
||||||
|
# AI_GATEWAY_API_KEY=...
|
||||||
|
|
||||||
# Langfuse Observability (Optional)
|
# Langfuse Observability (Optional)
|
||||||
# Enable LLM tracing and analytics - https://langfuse.com
|
# Enable LLM tracing and analytics - https://langfuse.com
|
||||||
# LANGFUSE_PUBLIC_KEY=pk-lf-...
|
# LANGFUSE_PUBLIC_KEY=pk-lf-...
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock"
|
|||||||
import { createAnthropic } from "@ai-sdk/anthropic"
|
import { createAnthropic } from "@ai-sdk/anthropic"
|
||||||
import { azure, createAzure } from "@ai-sdk/azure"
|
import { azure, createAzure } from "@ai-sdk/azure"
|
||||||
import { createDeepSeek, deepseek } from "@ai-sdk/deepseek"
|
import { createDeepSeek, deepseek } from "@ai-sdk/deepseek"
|
||||||
|
import { gateway } from "@ai-sdk/gateway"
|
||||||
import { createGoogleGenerativeAI, google } from "@ai-sdk/google"
|
import { createGoogleGenerativeAI, google } from "@ai-sdk/google"
|
||||||
import { createOpenAI, openai } from "@ai-sdk/openai"
|
import { createOpenAI, openai } from "@ai-sdk/openai"
|
||||||
import { fromNodeProviderChain } from "@aws-sdk/credential-providers"
|
import { fromNodeProviderChain } from "@aws-sdk/credential-providers"
|
||||||
@@ -18,6 +19,7 @@ export type ProviderName =
|
|||||||
| "openrouter"
|
| "openrouter"
|
||||||
| "deepseek"
|
| "deepseek"
|
||||||
| "siliconflow"
|
| "siliconflow"
|
||||||
|
| "gateway"
|
||||||
|
|
||||||
interface ModelConfig {
|
interface ModelConfig {
|
||||||
model: any
|
model: any
|
||||||
@@ -42,6 +44,7 @@ const ALLOWED_CLIENT_PROVIDERS: ProviderName[] = [
|
|||||||
"openrouter",
|
"openrouter",
|
||||||
"deepseek",
|
"deepseek",
|
||||||
"siliconflow",
|
"siliconflow",
|
||||||
|
"gateway",
|
||||||
]
|
]
|
||||||
|
|
||||||
// Bedrock provider options for Anthropic beta features
|
// Bedrock provider options for Anthropic beta features
|
||||||
@@ -333,8 +336,10 @@ function buildProviderOptions(
|
|||||||
|
|
||||||
case "deepseek":
|
case "deepseek":
|
||||||
case "openrouter":
|
case "openrouter":
|
||||||
case "siliconflow": {
|
case "siliconflow":
|
||||||
|
case "gateway": {
|
||||||
// These providers don't have reasoning configs in AI SDK yet
|
// These providers don't have reasoning configs in AI SDK yet
|
||||||
|
// Gateway passes through to underlying providers which handle their own configs
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,6 +361,7 @@ const PROVIDER_ENV_VARS: Record<ProviderName, string | null> = {
|
|||||||
openrouter: "OPENROUTER_API_KEY",
|
openrouter: "OPENROUTER_API_KEY",
|
||||||
deepseek: "DEEPSEEK_API_KEY",
|
deepseek: "DEEPSEEK_API_KEY",
|
||||||
siliconflow: "SILICONFLOW_API_KEY",
|
siliconflow: "SILICONFLOW_API_KEY",
|
||||||
|
gateway: "AI_GATEWAY_API_KEY",
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -495,6 +501,7 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
|||||||
if (configured.length === 0) {
|
if (configured.length === 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`No AI provider configured. Please set one of the following API keys in your .env.local file:\n` +
|
`No AI provider configured. Please set one of the following API keys in your .env.local file:\n` +
|
||||||
|
`- AI_GATEWAY_API_KEY for Vercel AI Gateway\n` +
|
||||||
`- DEEPSEEK_API_KEY for DeepSeek\n` +
|
`- DEEPSEEK_API_KEY for DeepSeek\n` +
|
||||||
`- OPENAI_API_KEY for OpenAI\n` +
|
`- OPENAI_API_KEY for OpenAI\n` +
|
||||||
`- ANTHROPIC_API_KEY for Anthropic\n` +
|
`- ANTHROPIC_API_KEY for Anthropic\n` +
|
||||||
@@ -672,9 +679,17 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "gateway": {
|
||||||
|
// Vercel AI Gateway - unified access to multiple AI providers
|
||||||
|
// Model format: "provider/model" e.g., "openai/gpt-4o", "anthropic/claude-sonnet-4-5"
|
||||||
|
// See: https://vercel.com/ai-gateway
|
||||||
|
model = gateway(modelId)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow`,
|
`Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow, gateway`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
47
package-lock.json
generated
47
package-lock.json
generated
@@ -1,18 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "next-ai-draw-io",
|
"name": "next-ai-draw-io",
|
||||||
"version": "0.4.0",
|
"version": "0.4.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "next-ai-draw-io",
|
"name": "next-ai-draw-io",
|
||||||
"version": "0.4.0",
|
"version": "0.4.2",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/amazon-bedrock": "^3.0.70",
|
"@ai-sdk/amazon-bedrock": "^3.0.70",
|
||||||
"@ai-sdk/anthropic": "^2.0.44",
|
"@ai-sdk/anthropic": "^2.0.44",
|
||||||
"@ai-sdk/azure": "^2.0.69",
|
"@ai-sdk/azure": "^2.0.69",
|
||||||
"@ai-sdk/deepseek": "^1.0.30",
|
"@ai-sdk/deepseek": "^1.0.30",
|
||||||
|
"@ai-sdk/gateway": "^2.0.21",
|
||||||
"@ai-sdk/google": "^2.0.0",
|
"@ai-sdk/google": "^2.0.0",
|
||||||
"@ai-sdk/openai": "^2.0.19",
|
"@ai-sdk/openai": "^2.0.19",
|
||||||
"@ai-sdk/react": "^2.0.107",
|
"@ai-sdk/react": "^2.0.107",
|
||||||
@@ -199,13 +200,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ai-sdk/gateway": {
|
"node_modules/@ai-sdk/gateway": {
|
||||||
"version": "2.0.18",
|
"version": "2.0.21",
|
||||||
"resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.18.tgz",
|
"resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.21.tgz",
|
||||||
"integrity": "sha512-sDQcW+6ck2m0pTIHW6BPHD7S125WD3qNkx/B8sEzJp/hurocmJ5Cni0ybExg6sQMGo+fr/GWOwpHF1cmCdg5rQ==",
|
"integrity": "sha512-BwV7DU/lAm3Xn6iyyvZdWgVxgLu3SNXzl5y57gMvkW4nGhAOV5269IrJzQwGt03bb107sa6H6uJwWxc77zXoGA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "2.0.0",
|
"@ai-sdk/provider": "2.0.0",
|
||||||
"@ai-sdk/provider-utils": "3.0.18",
|
"@ai-sdk/provider-utils": "3.0.19",
|
||||||
"@vercel/oidc": "3.0.5"
|
"@vercel/oidc": "3.0.5"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -215,6 +216,23 @@
|
|||||||
"zod": "^3.25.76 || ^4.1.8"
|
"zod": "^3.25.76 || ^4.1.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@ai-sdk/gateway/node_modules/@ai-sdk/provider-utils": {
|
||||||
|
"version": "3.0.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.19.tgz",
|
||||||
|
"integrity": "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@ai-sdk/provider": "2.0.0",
|
||||||
|
"@standard-schema/spec": "^1.0.0",
|
||||||
|
"eventsource-parser": "^3.0.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"zod": "^3.25.76 || ^4.1.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@ai-sdk/google": {
|
"node_modules/@ai-sdk/google": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@ai-sdk/google/-/google-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ai-sdk/google/-/google-2.0.0.tgz",
|
||||||
@@ -6130,6 +6148,23 @@
|
|||||||
"zod": "^3.25.76 || ^4.1.8"
|
"zod": "^3.25.76 || ^4.1.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ai/node_modules/@ai-sdk/gateway": {
|
||||||
|
"version": "2.0.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.18.tgz",
|
||||||
|
"integrity": "sha512-sDQcW+6ck2m0pTIHW6BPHD7S125WD3qNkx/B8sEzJp/hurocmJ5Cni0ybExg6sQMGo+fr/GWOwpHF1cmCdg5rQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@ai-sdk/provider": "2.0.0",
|
||||||
|
"@ai-sdk/provider-utils": "3.0.18",
|
||||||
|
"@vercel/oidc": "3.0.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"zod": "^3.25.76 || ^4.1.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.12.6",
|
"version": "6.12.6",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "next-ai-draw-io",
|
"name": "next-ai-draw-io",
|
||||||
"version": "0.4.2",
|
"version": "0.4.3",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
"@ai-sdk/anthropic": "^2.0.44",
|
"@ai-sdk/anthropic": "^2.0.44",
|
||||||
"@ai-sdk/azure": "^2.0.69",
|
"@ai-sdk/azure": "^2.0.69",
|
||||||
"@ai-sdk/deepseek": "^1.0.30",
|
"@ai-sdk/deepseek": "^1.0.30",
|
||||||
|
"@ai-sdk/gateway": "^2.0.21",
|
||||||
"@ai-sdk/google": "^2.0.0",
|
"@ai-sdk/google": "^2.0.0",
|
||||||
"@ai-sdk/openai": "^2.0.19",
|
"@ai-sdk/openai": "^2.0.19",
|
||||||
"@ai-sdk/react": "^2.0.107",
|
"@ai-sdk/react": "^2.0.107",
|
||||||
|
|||||||
162
packages/mcp-server/README.md
Normal file
162
packages/mcp-server/README.md
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
# Next AI Draw.io MCP Server
|
||||||
|
|
||||||
|
MCP (Model Context Protocol) server that enables AI agents like Claude Desktop and Cursor to generate and edit draw.io diagrams with **real-time browser preview**.
|
||||||
|
|
||||||
|
**Self-contained** - includes an embedded HTTP server, no external dependencies required.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"drawio": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["@next-ai-drawio/mcp-server@latest"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Claude Desktop
|
||||||
|
|
||||||
|
Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"drawio": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["@next-ai-drawio/mcp-server@latest"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### VS Code
|
||||||
|
|
||||||
|
Add to your VS Code settings (`.vscode/mcp.json` in workspace or user settings):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"drawio": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["@next-ai-drawio/mcp-server@latest"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cursor
|
||||||
|
|
||||||
|
Add to Cursor MCP config (`~/.cursor/mcp.json`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"drawio": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["@next-ai-drawio/mcp-server@latest"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Claude Code CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude mcp add drawio -- npx @next-ai-drawio/mcp-server@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Other MCP Clients
|
||||||
|
|
||||||
|
Use the standard MCP configuration with:
|
||||||
|
- **Command**: `npx`
|
||||||
|
- **Args**: `["@next-ai-drawio/mcp-server@latest"]`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Restart your MCP client after updating config
|
||||||
|
2. Ask the AI to create a diagram:
|
||||||
|
> "Create a flowchart showing user authentication with login, MFA, and session management"
|
||||||
|
3. The diagram appears in your browser in real-time!
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Real-time Preview**: Diagrams appear and update in your browser as the AI creates them
|
||||||
|
- **Natural Language**: Describe diagrams in plain text - flowcharts, architecture diagrams, etc.
|
||||||
|
- **Edit Support**: Modify existing diagrams with natural language instructions
|
||||||
|
- **Export**: Save diagrams as `.drawio` files
|
||||||
|
- **Self-contained**: Embedded server, works offline (except draw.io UI which loads from embed.diagrams.net)
|
||||||
|
|
||||||
|
## Available Tools
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `start_session` | Opens browser with real-time diagram preview |
|
||||||
|
| `display_diagram` | Create a new diagram from XML |
|
||||||
|
| `edit_diagram` | Edit diagram by ID-based operations (update/add/delete cells) |
|
||||||
|
| `get_diagram` | Get the current diagram XML |
|
||||||
|
| `export_diagram` | Save diagram to a `.drawio` file |
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐ stdio ┌─────────────────┐
|
||||||
|
│ Claude Desktop │ <───────────> │ MCP Server │
|
||||||
|
│ (AI Agent) │ │ (this package) │
|
||||||
|
└─────────────────┘ └────────┬────────┘
|
||||||
|
│
|
||||||
|
┌────────▼────────┐
|
||||||
|
│ Embedded HTTP │
|
||||||
|
│ Server (:6002) │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
┌────────▼────────┐
|
||||||
|
│ User's Browser │
|
||||||
|
│ (draw.io embed) │
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **MCP Server** receives tool calls from Claude via stdio
|
||||||
|
2. **Embedded HTTP Server** serves the draw.io UI and handles state
|
||||||
|
3. **Browser** shows real-time diagram updates via polling
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `PORT` | `6002` | Port for the embedded HTTP server |
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Port already in use
|
||||||
|
|
||||||
|
If port 6002 is in use, the server will automatically try the next available port (up to 6020).
|
||||||
|
|
||||||
|
Or set a custom port:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"drawio": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["@next-ai-drawio/mcp-server@latest"],
|
||||||
|
"env": { "PORT": "6003" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### "No active session"
|
||||||
|
|
||||||
|
Call `start_session` first to open the browser window.
|
||||||
|
|
||||||
|
### Browser not updating
|
||||||
|
|
||||||
|
Check that the browser URL has the `?mcp=` query parameter. The MCP session ID connects the browser to the server.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Apache-2.0
|
||||||
2044
packages/mcp-server/package-lock.json
generated
Normal file
2044
packages/mcp-server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
55
packages/mcp-server/package.json
Normal file
55
packages/mcp-server/package.json
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"name": "@next-ai-drawio/mcp-server",
|
||||||
|
"version": "0.1.2",
|
||||||
|
"description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"bin": {
|
||||||
|
"next-ai-drawio-mcp": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"dev": "tsx watch src/index.ts",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"mcp",
|
||||||
|
"drawio",
|
||||||
|
"diagram",
|
||||||
|
"ai",
|
||||||
|
"claude",
|
||||||
|
"model-context-protocol"
|
||||||
|
],
|
||||||
|
"author": "Biki-dev",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Biki-dev/next-ai-draw-io",
|
||||||
|
"directory": "packages/mcp-server"
|
||||||
|
},
|
||||||
|
"homepage": "https://next-ai-drawio.jiang.jp",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/Biki-dev/next-ai-draw-io/issues"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||||
|
"linkedom": "^0.18.0",
|
||||||
|
"open": "^10.1.0",
|
||||||
|
"zod": "^3.24.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20",
|
||||||
|
"tsx": "^4.19.0",
|
||||||
|
"typescript": "^5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
]
|
||||||
|
}
|
||||||
219
packages/mcp-server/src/diagram-operations.ts
Normal file
219
packages/mcp-server/src/diagram-operations.ts
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
/**
|
||||||
|
* ID-based diagram operations
|
||||||
|
* Copied from lib/utils.ts to avoid cross-package imports
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface DiagramOperation {
|
||||||
|
type: "update" | "add" | "delete"
|
||||||
|
cell_id: string
|
||||||
|
new_xml?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OperationError {
|
||||||
|
type: "update" | "add" | "delete"
|
||||||
|
cellId: string
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApplyOperationsResult {
|
||||||
|
result: string
|
||||||
|
errors: OperationError[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply diagram operations (update/add/delete) using ID-based lookup.
|
||||||
|
* This replaces the text-matching approach with direct DOM manipulation.
|
||||||
|
*
|
||||||
|
* @param xmlContent - The full mxfile XML content
|
||||||
|
* @param operations - Array of operations to apply
|
||||||
|
* @returns Object with result XML and any errors
|
||||||
|
*/
|
||||||
|
export function applyDiagramOperations(
|
||||||
|
xmlContent: string,
|
||||||
|
operations: DiagramOperation[],
|
||||||
|
): ApplyOperationsResult {
|
||||||
|
const errors: OperationError[] = []
|
||||||
|
|
||||||
|
// Parse the XML
|
||||||
|
const parser = new DOMParser()
|
||||||
|
const doc = parser.parseFromString(xmlContent, "text/xml")
|
||||||
|
|
||||||
|
// Check for parse errors
|
||||||
|
const parseError = doc.querySelector("parsererror")
|
||||||
|
if (parseError) {
|
||||||
|
return {
|
||||||
|
result: xmlContent,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
type: "update",
|
||||||
|
cellId: "",
|
||||||
|
message: `XML parse error: ${parseError.textContent}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the root element (inside mxGraphModel)
|
||||||
|
const root = doc.querySelector("root")
|
||||||
|
if (!root) {
|
||||||
|
return {
|
||||||
|
result: xmlContent,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
type: "update",
|
||||||
|
cellId: "",
|
||||||
|
message: "Could not find <root> element in XML",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a map of cell IDs to elements
|
||||||
|
const cellMap = new Map<string, Element>()
|
||||||
|
root.querySelectorAll("mxCell").forEach((cell) => {
|
||||||
|
const id = cell.getAttribute("id")
|
||||||
|
if (id) cellMap.set(id, cell)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Process each operation
|
||||||
|
for (const op of operations) {
|
||||||
|
if (op.type === "update") {
|
||||||
|
const existingCell = cellMap.get(op.cell_id)
|
||||||
|
if (!existingCell) {
|
||||||
|
errors.push({
|
||||||
|
type: "update",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: `Cell with id="${op.cell_id}" not found`,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!op.new_xml) {
|
||||||
|
errors.push({
|
||||||
|
type: "update",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: "new_xml is required for update operation",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the new XML
|
||||||
|
const newDoc = parser.parseFromString(
|
||||||
|
`<wrapper>${op.new_xml}</wrapper>`,
|
||||||
|
"text/xml",
|
||||||
|
)
|
||||||
|
const newCell = newDoc.querySelector("mxCell")
|
||||||
|
if (!newCell) {
|
||||||
|
errors.push({
|
||||||
|
type: "update",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: "new_xml must contain an mxCell element",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate ID matches
|
||||||
|
const newCellId = newCell.getAttribute("id")
|
||||||
|
if (newCellId !== op.cell_id) {
|
||||||
|
errors.push({
|
||||||
|
type: "update",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: `ID mismatch: cell_id is "${op.cell_id}" but new_xml has id="${newCellId}"`,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import and replace the node
|
||||||
|
const importedNode = doc.importNode(newCell, true)
|
||||||
|
existingCell.parentNode?.replaceChild(importedNode, existingCell)
|
||||||
|
|
||||||
|
// Update the map with the new element
|
||||||
|
cellMap.set(op.cell_id, importedNode)
|
||||||
|
} else if (op.type === "add") {
|
||||||
|
// Check if ID already exists
|
||||||
|
if (cellMap.has(op.cell_id)) {
|
||||||
|
errors.push({
|
||||||
|
type: "add",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: `Cell with id="${op.cell_id}" already exists`,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!op.new_xml) {
|
||||||
|
errors.push({
|
||||||
|
type: "add",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: "new_xml is required for add operation",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the new XML
|
||||||
|
const newDoc = parser.parseFromString(
|
||||||
|
`<wrapper>${op.new_xml}</wrapper>`,
|
||||||
|
"text/xml",
|
||||||
|
)
|
||||||
|
const newCell = newDoc.querySelector("mxCell")
|
||||||
|
if (!newCell) {
|
||||||
|
errors.push({
|
||||||
|
type: "add",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: "new_xml must contain an mxCell element",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate ID matches
|
||||||
|
const newCellId = newCell.getAttribute("id")
|
||||||
|
if (newCellId !== op.cell_id) {
|
||||||
|
errors.push({
|
||||||
|
type: "add",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: `ID mismatch: cell_id is "${op.cell_id}" but new_xml has id="${newCellId}"`,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import and append the node
|
||||||
|
const importedNode = doc.importNode(newCell, true)
|
||||||
|
root.appendChild(importedNode)
|
||||||
|
|
||||||
|
// Add to map
|
||||||
|
cellMap.set(op.cell_id, importedNode)
|
||||||
|
} else if (op.type === "delete") {
|
||||||
|
const existingCell = cellMap.get(op.cell_id)
|
||||||
|
if (!existingCell) {
|
||||||
|
errors.push({
|
||||||
|
type: "delete",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: `Cell with id="${op.cell_id}" not found`,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for edges referencing this cell (warning only, still delete)
|
||||||
|
const referencingEdges = root.querySelectorAll(
|
||||||
|
`mxCell[source="${op.cell_id}"], mxCell[target="${op.cell_id}"]`,
|
||||||
|
)
|
||||||
|
if (referencingEdges.length > 0) {
|
||||||
|
const edgeIds = Array.from(referencingEdges)
|
||||||
|
.map((e) => e.getAttribute("id"))
|
||||||
|
.join(", ")
|
||||||
|
console.warn(
|
||||||
|
`[applyDiagramOperations] Deleting cell "${op.cell_id}" which is referenced by edges: ${edgeIds}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the node
|
||||||
|
existingCell.parentNode?.removeChild(existingCell)
|
||||||
|
cellMap.delete(op.cell_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize back to string
|
||||||
|
const serializer = new XMLSerializer()
|
||||||
|
const result = serializer.serializeToString(doc)
|
||||||
|
|
||||||
|
return { result, errors }
|
||||||
|
}
|
||||||
384
packages/mcp-server/src/http-server.ts
Normal file
384
packages/mcp-server/src/http-server.ts
Normal file
@@ -0,0 +1,384 @@
|
|||||||
|
/**
|
||||||
|
* Embedded HTTP Server for MCP
|
||||||
|
*
|
||||||
|
* Serves a static HTML page with draw.io embed and handles state sync.
|
||||||
|
* This eliminates the need for an external Next.js app.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import http from "node:http"
|
||||||
|
import { log } from "./logger.js"
|
||||||
|
|
||||||
|
interface SessionState {
|
||||||
|
xml: string
|
||||||
|
version: number
|
||||||
|
lastUpdated: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
// In-memory state store (shared with MCP server in same process)
|
||||||
|
export const stateStore = new Map<string, SessionState>()
|
||||||
|
|
||||||
|
let server: http.Server | null = null
|
||||||
|
let serverPort: number = 6002
|
||||||
|
const MAX_PORT = 6020 // Don't retry beyond this port
|
||||||
|
const SESSION_TTL = 60 * 60 * 1000 // 1 hour
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get state for a session
|
||||||
|
*/
|
||||||
|
export function getState(sessionId: string): SessionState | undefined {
|
||||||
|
return stateStore.get(sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set state for a session
|
||||||
|
*/
|
||||||
|
export function setState(sessionId: string, xml: string): number {
|
||||||
|
const existing = stateStore.get(sessionId)
|
||||||
|
const newVersion = (existing?.version || 0) + 1
|
||||||
|
|
||||||
|
stateStore.set(sessionId, {
|
||||||
|
xml,
|
||||||
|
version: newVersion,
|
||||||
|
lastUpdated: new Date(),
|
||||||
|
})
|
||||||
|
|
||||||
|
log.debug(`State updated: session=${sessionId}, version=${newVersion}`)
|
||||||
|
return newVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the embedded HTTP server
|
||||||
|
*/
|
||||||
|
export function startHttpServer(port: number = 6002): Promise<number> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (server) {
|
||||||
|
resolve(serverPort)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serverPort = port
|
||||||
|
server = http.createServer(handleRequest)
|
||||||
|
|
||||||
|
server.on("error", (err: NodeJS.ErrnoException) => {
|
||||||
|
if (err.code === "EADDRINUSE") {
|
||||||
|
if (port >= MAX_PORT) {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`No available ports in range 6002-${MAX_PORT}`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.info(`Port ${port} in use, trying ${port + 1}`)
|
||||||
|
server = null
|
||||||
|
startHttpServer(port + 1)
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject)
|
||||||
|
} else {
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
server.listen(port, () => {
|
||||||
|
serverPort = port
|
||||||
|
log.info(`Embedded HTTP server running on http://localhost:${port}`)
|
||||||
|
resolve(port)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the HTTP server
|
||||||
|
*/
|
||||||
|
export function stopHttpServer(): void {
|
||||||
|
if (server) {
|
||||||
|
server.close()
|
||||||
|
server = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up expired sessions
|
||||||
|
*/
|
||||||
|
function cleanupExpiredSessions(): void {
|
||||||
|
const now = Date.now()
|
||||||
|
for (const [sessionId, state] of stateStore) {
|
||||||
|
if (now - state.lastUpdated.getTime() > SESSION_TTL) {
|
||||||
|
stateStore.delete(sessionId)
|
||||||
|
log.info(`Cleaned up expired session: ${sessionId}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run cleanup every 5 minutes
|
||||||
|
setInterval(cleanupExpiredSessions, 5 * 60 * 1000)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current server port
|
||||||
|
*/
|
||||||
|
export function getServerPort(): number {
|
||||||
|
return serverPort
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle HTTP requests
|
||||||
|
*/
|
||||||
|
function handleRequest(
|
||||||
|
req: http.IncomingMessage,
|
||||||
|
res: http.ServerResponse,
|
||||||
|
): void {
|
||||||
|
const url = new URL(req.url || "/", `http://localhost:${serverPort}`)
|
||||||
|
|
||||||
|
// CORS headers for local development
|
||||||
|
res.setHeader("Access-Control-Allow-Origin", "*")
|
||||||
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||||
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type")
|
||||||
|
|
||||||
|
if (req.method === "OPTIONS") {
|
||||||
|
res.writeHead(204)
|
||||||
|
res.end()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route handling
|
||||||
|
if (url.pathname === "/" || url.pathname === "/index.html") {
|
||||||
|
serveHtml(req, res, url)
|
||||||
|
} else if (
|
||||||
|
url.pathname === "/api/state" ||
|
||||||
|
url.pathname === "/api/mcp/state"
|
||||||
|
) {
|
||||||
|
handleStateApi(req, res, url)
|
||||||
|
} else if (
|
||||||
|
url.pathname === "/api/health" ||
|
||||||
|
url.pathname === "/api/mcp/health"
|
||||||
|
) {
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" })
|
||||||
|
res.end(JSON.stringify({ status: "ok", mcp: true }))
|
||||||
|
} else {
|
||||||
|
res.writeHead(404)
|
||||||
|
res.end("Not Found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serve the HTML page with draw.io embed
|
||||||
|
*/
|
||||||
|
function serveHtml(
|
||||||
|
req: http.IncomingMessage,
|
||||||
|
res: http.ServerResponse,
|
||||||
|
url: URL,
|
||||||
|
): void {
|
||||||
|
const sessionId = url.searchParams.get("mcp") || ""
|
||||||
|
|
||||||
|
res.writeHead(200, { "Content-Type": "text/html" })
|
||||||
|
res.end(getHtmlPage(sessionId))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle state API requests
|
||||||
|
*/
|
||||||
|
function handleStateApi(
|
||||||
|
req: http.IncomingMessage,
|
||||||
|
res: http.ServerResponse,
|
||||||
|
url: URL,
|
||||||
|
): void {
|
||||||
|
if (req.method === "GET") {
|
||||||
|
const sessionId = url.searchParams.get("sessionId")
|
||||||
|
if (!sessionId) {
|
||||||
|
res.writeHead(400, { "Content-Type": "application/json" })
|
||||||
|
res.end(JSON.stringify({ error: "sessionId required" }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = stateStore.get(sessionId)
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" })
|
||||||
|
res.end(
|
||||||
|
JSON.stringify({
|
||||||
|
xml: state?.xml || null,
|
||||||
|
version: state?.version || 0,
|
||||||
|
lastUpdated: state?.lastUpdated?.toISOString() || null,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
} else if (req.method === "POST") {
|
||||||
|
let body = ""
|
||||||
|
req.on("data", (chunk) => {
|
||||||
|
body += chunk
|
||||||
|
})
|
||||||
|
req.on("end", () => {
|
||||||
|
try {
|
||||||
|
const { sessionId, xml } = JSON.parse(body)
|
||||||
|
if (!sessionId) {
|
||||||
|
res.writeHead(400, { "Content-Type": "application/json" })
|
||||||
|
res.end(JSON.stringify({ error: "sessionId required" }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const version = setState(sessionId, xml)
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" })
|
||||||
|
res.end(JSON.stringify({ success: true, version }))
|
||||||
|
} catch {
|
||||||
|
res.writeHead(400, { "Content-Type": "application/json" })
|
||||||
|
res.end(JSON.stringify({ error: "Invalid JSON" }))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
res.writeHead(405)
|
||||||
|
res.end("Method Not Allowed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the HTML page with draw.io embed
|
||||||
|
*/
|
||||||
|
function getHtmlPage(sessionId: string): string {
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Draw.io MCP - ${sessionId || "No Session"}</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
html, body { width: 100%; height: 100%; overflow: hidden; }
|
||||||
|
#container { width: 100%; height: 100%; display: flex; flex-direction: column; }
|
||||||
|
#header {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: #1a1a2e;
|
||||||
|
color: #eee;
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
#header .session { color: #888; font-size: 12px; }
|
||||||
|
#header .status { font-size: 12px; }
|
||||||
|
#header .status.connected { color: #4ade80; }
|
||||||
|
#header .status.disconnected { color: #f87171; }
|
||||||
|
#drawio { flex: 1; border: none; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="container">
|
||||||
|
<div id="header">
|
||||||
|
<div>
|
||||||
|
<strong>Draw.io MCP</strong>
|
||||||
|
<span class="session">${sessionId ? `Session: ${sessionId}` : "No MCP session"}</span>
|
||||||
|
</div>
|
||||||
|
<div id="status" class="status disconnected">Connecting...</div>
|
||||||
|
</div>
|
||||||
|
<iframe id="drawio" src="https://embed.diagrams.net/?embed=1&proto=json&spin=1&libraries=1"></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const sessionId = "${sessionId}";
|
||||||
|
const iframe = document.getElementById('drawio');
|
||||||
|
const statusEl = document.getElementById('status');
|
||||||
|
|
||||||
|
let currentVersion = 0;
|
||||||
|
let isDrawioReady = false;
|
||||||
|
let pendingXml = null;
|
||||||
|
let lastLoadedXml = null;
|
||||||
|
|
||||||
|
// Listen for messages from draw.io
|
||||||
|
window.addEventListener('message', (event) => {
|
||||||
|
if (event.origin !== 'https://embed.diagrams.net') return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(event.data);
|
||||||
|
handleDrawioMessage(msg);
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore non-JSON messages
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleDrawioMessage(msg) {
|
||||||
|
if (msg.event === 'init') {
|
||||||
|
isDrawioReady = true;
|
||||||
|
statusEl.textContent = 'Ready';
|
||||||
|
statusEl.className = 'status connected';
|
||||||
|
|
||||||
|
// Load pending XML if any
|
||||||
|
if (pendingXml) {
|
||||||
|
loadDiagram(pendingXml);
|
||||||
|
pendingXml = null;
|
||||||
|
}
|
||||||
|
} else if (msg.event === 'save') {
|
||||||
|
// User saved - push to state
|
||||||
|
if (msg.xml && msg.xml !== lastLoadedXml) {
|
||||||
|
pushState(msg.xml);
|
||||||
|
}
|
||||||
|
} else if (msg.event === 'export') {
|
||||||
|
// Export completed
|
||||||
|
if (msg.data) {
|
||||||
|
pushState(msg.data);
|
||||||
|
}
|
||||||
|
} else if (msg.event === 'autosave') {
|
||||||
|
// Autosave - push to state
|
||||||
|
if (msg.xml && msg.xml !== lastLoadedXml) {
|
||||||
|
pushState(msg.xml);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadDiagram(xml) {
|
||||||
|
if (!isDrawioReady) {
|
||||||
|
pendingXml = xml;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastLoadedXml = xml;
|
||||||
|
iframe.contentWindow.postMessage(JSON.stringify({
|
||||||
|
action: 'load',
|
||||||
|
xml: xml,
|
||||||
|
autosave: 1
|
||||||
|
}), '*');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pushState(xml) {
|
||||||
|
if (!sessionId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/state', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ sessionId, xml })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
currentVersion = result.version;
|
||||||
|
lastLoadedXml = xml;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to push state:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pollState() {
|
||||||
|
if (!sessionId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/state?sessionId=' + encodeURIComponent(sessionId));
|
||||||
|
if (!response.ok) return;
|
||||||
|
|
||||||
|
const state = await response.json();
|
||||||
|
|
||||||
|
if (state.version && state.version > currentVersion && state.xml) {
|
||||||
|
currentVersion = state.version;
|
||||||
|
loadDiagram(state.xml);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to poll state:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start polling if we have a session
|
||||||
|
if (sessionId) {
|
||||||
|
pollState();
|
||||||
|
setInterval(pollState, 2000);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
}
|
||||||
476
packages/mcp-server/src/index.ts
Normal file
476
packages/mcp-server/src/index.ts
Normal file
@@ -0,0 +1,476 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* MCP Server for Next AI Draw.io
|
||||||
|
*
|
||||||
|
* Enables AI agents (Claude Desktop, Cursor, etc.) to generate and edit
|
||||||
|
* draw.io diagrams with real-time browser preview.
|
||||||
|
*
|
||||||
|
* Uses an embedded HTTP server - no external dependencies required.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Setup DOM polyfill for Node.js (required for XML operations)
|
||||||
|
import { DOMParser } from "linkedom"
|
||||||
|
;(globalThis as any).DOMParser = DOMParser
|
||||||
|
|
||||||
|
// Create XMLSerializer polyfill using outerHTML
|
||||||
|
class XMLSerializerPolyfill {
|
||||||
|
serializeToString(node: any): string {
|
||||||
|
if (node.outerHTML !== undefined) {
|
||||||
|
return node.outerHTML
|
||||||
|
}
|
||||||
|
if (node.documentElement) {
|
||||||
|
return node.documentElement.outerHTML
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
;(globalThis as any).XMLSerializer = XMLSerializerPolyfill
|
||||||
|
|
||||||
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
|
||||||
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
||||||
|
import open from "open"
|
||||||
|
import { z } from "zod"
|
||||||
|
import {
|
||||||
|
applyDiagramOperations,
|
||||||
|
type DiagramOperation,
|
||||||
|
} from "./diagram-operations.js"
|
||||||
|
import {
|
||||||
|
getServerPort,
|
||||||
|
getState,
|
||||||
|
setState,
|
||||||
|
startHttpServer,
|
||||||
|
} from "./http-server.js"
|
||||||
|
import { log } from "./logger.js"
|
||||||
|
|
||||||
|
// Server configuration
|
||||||
|
const config = {
|
||||||
|
port: parseInt(process.env.PORT || "6002"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session state (single session for simplicity)
|
||||||
|
let currentSession: {
|
||||||
|
id: string
|
||||||
|
xml: string
|
||||||
|
version: number
|
||||||
|
} | null = null
|
||||||
|
|
||||||
|
// Create MCP server
|
||||||
|
const server = new McpServer({
|
||||||
|
name: "next-ai-drawio",
|
||||||
|
version: "0.1.2",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Register prompt with workflow guidance
|
||||||
|
server.prompt(
|
||||||
|
"diagram-workflow",
|
||||||
|
"Guidelines for creating and editing draw.io diagrams",
|
||||||
|
() => ({
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: {
|
||||||
|
type: "text",
|
||||||
|
text: `# Draw.io Diagram Workflow Guidelines
|
||||||
|
|
||||||
|
## Creating a New Diagram
|
||||||
|
1. Call start_session to open the browser preview
|
||||||
|
2. Use display_diagram with complete mxGraphModel XML to create a new diagram
|
||||||
|
|
||||||
|
## Adding Elements to Existing Diagram
|
||||||
|
1. Use edit_diagram with "add" operation
|
||||||
|
2. Provide a unique cell_id and complete mxCell XML
|
||||||
|
3. No need to call get_diagram first - the server fetches latest state automatically
|
||||||
|
|
||||||
|
## Modifying or Deleting Existing Elements
|
||||||
|
1. FIRST call get_diagram to see current cell IDs and structure
|
||||||
|
2. THEN call edit_diagram with "update" or "delete" operations
|
||||||
|
3. For update, provide the cell_id and complete new mxCell XML
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
- display_diagram REPLACES the entire diagram - only use for new diagrams
|
||||||
|
- edit_diagram PRESERVES user's manual changes (fetches browser state first)
|
||||||
|
- Always use unique cell_ids when adding elements (e.g., "shape-1", "arrow-2")`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tool: start_session
|
||||||
|
server.registerTool(
|
||||||
|
"start_session",
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
"Start a new diagram session and open the browser for real-time preview. " +
|
||||||
|
"Starts an embedded server and opens a browser window with draw.io. " +
|
||||||
|
"The browser will show diagram updates as they happen.",
|
||||||
|
inputSchema: {},
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
// Start embedded HTTP server
|
||||||
|
const port = await startHttpServer(config.port)
|
||||||
|
|
||||||
|
// Create session
|
||||||
|
const sessionId = `mcp-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 8)}`
|
||||||
|
currentSession = {
|
||||||
|
id: sessionId,
|
||||||
|
xml: "",
|
||||||
|
version: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open browser
|
||||||
|
const browserUrl = `http://localhost:${port}?mcp=${sessionId}`
|
||||||
|
await open(browserUrl)
|
||||||
|
|
||||||
|
log.info(`Started session ${sessionId}, browser at ${browserUrl}`)
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Session started successfully!\n\nSession ID: ${sessionId}\nBrowser URL: ${browserUrl}\n\nThe browser will now show real-time diagram updates.`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
log.error("start_session failed:", message)
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Error: ${message}` }],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tool: display_diagram
|
||||||
|
server.registerTool(
|
||||||
|
"display_diagram",
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
"Display a NEW draw.io diagram from XML. REPLACES the entire diagram. " +
|
||||||
|
"Use this for creating new diagrams from scratch. " +
|
||||||
|
"To ADD elements to an existing diagram, use edit_diagram with 'add' operation instead. " +
|
||||||
|
"You should generate valid draw.io/mxGraph XML format.",
|
||||||
|
inputSchema: {
|
||||||
|
xml: z
|
||||||
|
.string()
|
||||||
|
.describe("The draw.io XML to display (mxGraphModel format)"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async ({ xml }) => {
|
||||||
|
try {
|
||||||
|
if (!currentSession) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Error: No active session. Please call start_session first.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`Displaying diagram, ${xml.length} chars`)
|
||||||
|
|
||||||
|
// Update session state
|
||||||
|
currentSession.xml = xml
|
||||||
|
currentSession.version++
|
||||||
|
|
||||||
|
// Push to embedded server state
|
||||||
|
setState(currentSession.id, xml)
|
||||||
|
|
||||||
|
log.info(`Diagram displayed successfully`)
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Diagram displayed successfully!\n\nThe diagram is now visible in your browser.\n\nXML length: ${xml.length} characters`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
log.error("display_diagram failed:", message)
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Error: ${message}` }],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tool: edit_diagram
|
||||||
|
server.registerTool(
|
||||||
|
"edit_diagram",
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
"Edit the current diagram by ID-based operations (update/add/delete cells). " +
|
||||||
|
"ALWAYS fetches the latest state from browser first, so user's manual changes are preserved.\n\n" +
|
||||||
|
"IMPORTANT workflow:\n" +
|
||||||
|
"- For ADD operations: Can use directly - just provide new unique cell_id and new_xml.\n" +
|
||||||
|
"- For UPDATE/DELETE: Call get_diagram FIRST to see current cell IDs, then edit.\n\n" +
|
||||||
|
"Operations:\n" +
|
||||||
|
"- add: Add a new cell. Provide cell_id (new unique id) and new_xml.\n" +
|
||||||
|
"- update: Replace an existing cell by its id. Provide cell_id and complete new_xml.\n" +
|
||||||
|
"- delete: Remove a cell by its id. Only cell_id is needed.\n\n" +
|
||||||
|
"For add/update, new_xml must be a complete mxCell element including mxGeometry.",
|
||||||
|
inputSchema: {
|
||||||
|
operations: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
type: z
|
||||||
|
.enum(["update", "add", "delete"])
|
||||||
|
.describe("Operation type"),
|
||||||
|
cell_id: z.string().describe("The id of the mxCell"),
|
||||||
|
new_xml: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
"Complete mxCell XML element (required for update/add)",
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.describe("Array of operations to apply"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async ({ operations }) => {
|
||||||
|
try {
|
||||||
|
if (!currentSession) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Error: No active session. Please call start_session first.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch latest state from browser
|
||||||
|
const browserState = getState(currentSession.id)
|
||||||
|
if (browserState?.xml) {
|
||||||
|
currentSession.xml = browserState.xml
|
||||||
|
log.info("Fetched latest diagram state from browser")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentSession.xml) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Error: No diagram to edit. Please create a diagram first with display_diagram.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`Editing diagram with ${operations.length} operation(s)`)
|
||||||
|
|
||||||
|
// Apply operations
|
||||||
|
const { result, errors } = applyDiagramOperations(
|
||||||
|
currentSession.xml,
|
||||||
|
operations as DiagramOperation[],
|
||||||
|
)
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
const errorMessages = errors
|
||||||
|
.map((e) => `${e.type} ${e.cellId}: ${e.message}`)
|
||||||
|
.join("\n")
|
||||||
|
log.warn(`Edit had ${errors.length} error(s): ${errorMessages}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
currentSession.xml = result
|
||||||
|
currentSession.version++
|
||||||
|
|
||||||
|
// Push to embedded server
|
||||||
|
setState(currentSession.id, result)
|
||||||
|
|
||||||
|
log.info(`Diagram edited successfully`)
|
||||||
|
|
||||||
|
const successMsg = `Diagram edited successfully!\n\nApplied ${operations.length} operation(s).`
|
||||||
|
const errorMsg =
|
||||||
|
errors.length > 0
|
||||||
|
? `\n\nWarnings:\n${errors.map((e) => `- ${e.type} ${e.cellId}: ${e.message}`).join("\n")}`
|
||||||
|
: ""
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: successMsg + errorMsg,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
log.error("edit_diagram failed:", message)
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Error: ${message}` }],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tool: get_diagram
|
||||||
|
server.registerTool(
|
||||||
|
"get_diagram",
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
"Get the current diagram XML (fetches latest from browser, including user's manual edits). " +
|
||||||
|
"Call this BEFORE edit_diagram if you need to update or delete existing elements, " +
|
||||||
|
"so you can see the current cell IDs and structure.",
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
if (!currentSession) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Error: No active session. Please call start_session first.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch latest state from browser
|
||||||
|
const browserState = getState(currentSession.id)
|
||||||
|
if (browserState?.xml) {
|
||||||
|
currentSession.xml = browserState.xml
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentSession.xml) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "No diagram exists yet. Use display_diagram to create one.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Current diagram XML:\n\n${currentSession.xml}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
log.error("get_diagram failed:", message)
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Error: ${message}` }],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tool: export_diagram
|
||||||
|
server.registerTool(
|
||||||
|
"export_diagram",
|
||||||
|
{
|
||||||
|
description: "Export the current diagram to a .drawio file.",
|
||||||
|
inputSchema: {
|
||||||
|
path: z
|
||||||
|
.string()
|
||||||
|
.describe(
|
||||||
|
"File path to save the diagram (e.g., ./diagram.drawio)",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async ({ path }) => {
|
||||||
|
try {
|
||||||
|
if (!currentSession) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Error: No active session. Please call start_session first.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch latest state
|
||||||
|
const browserState = getState(currentSession.id)
|
||||||
|
if (browserState?.xml) {
|
||||||
|
currentSession.xml = browserState.xml
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentSession.xml) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Error: No diagram to export. Please create a diagram first.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fs = await import("node:fs/promises")
|
||||||
|
const nodePath = await import("node:path")
|
||||||
|
|
||||||
|
let filePath = path
|
||||||
|
if (!filePath.endsWith(".drawio")) {
|
||||||
|
filePath = `${filePath}.drawio`
|
||||||
|
}
|
||||||
|
|
||||||
|
const absolutePath = nodePath.resolve(filePath)
|
||||||
|
await fs.writeFile(absolutePath, currentSession.xml, "utf-8")
|
||||||
|
|
||||||
|
log.info(`Diagram exported to ${absolutePath}`)
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Diagram exported successfully!\n\nFile: ${absolutePath}\nSize: ${currentSession.xml.length} characters`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
log.error("export_diagram failed:", message)
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Error: ${message}` }],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Start the MCP server
|
||||||
|
async function main() {
|
||||||
|
log.info("Starting MCP server for Next AI Draw.io (embedded mode)...")
|
||||||
|
|
||||||
|
const transport = new StdioServerTransport()
|
||||||
|
await server.connect(transport)
|
||||||
|
|
||||||
|
log.info("MCP server running on stdio")
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
log.error("Fatal error:", error)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
24
packages/mcp-server/src/logger.ts
Normal file
24
packages/mcp-server/src/logger.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Logger for MCP server
|
||||||
|
*
|
||||||
|
* CRITICAL: MCP servers communicate via STDIO (stdin/stdout).
|
||||||
|
* Using console.log() will corrupt the JSON-RPC protocol messages.
|
||||||
|
* ALL logging MUST use console.error() which writes to stderr.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const log = {
|
||||||
|
info: (msg: string, ...args: unknown[]) => {
|
||||||
|
console.error(`[MCP-DrawIO] [INFO] ${msg}`, ...args)
|
||||||
|
},
|
||||||
|
error: (msg: string, ...args: unknown[]) => {
|
||||||
|
console.error(`[MCP-DrawIO] [ERROR] ${msg}`, ...args)
|
||||||
|
},
|
||||||
|
debug: (msg: string, ...args: unknown[]) => {
|
||||||
|
if (process.env.DEBUG === "true") {
|
||||||
|
console.error(`[MCP-DrawIO] [DEBUG] ${msg}`, ...args)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
warn: (msg: string, ...args: unknown[]) => {
|
||||||
|
console.error(`[MCP-DrawIO] [WARN] ${msg}`, ...args)
|
||||||
|
},
|
||||||
|
}
|
||||||
19
packages/mcp-server/tsconfig.json
Normal file
19
packages/mcp-server/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "Node16",
|
||||||
|
"moduleResolution": "Node16",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
BIN
public/favicon-192x192.png
Normal file
BIN
public/favicon-192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
public/favicon-512x512.png
Normal file
BIN
public/favicon-512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
@@ -29,5 +29,5 @@
|
|||||||
".next/types/**/*.ts",
|
".next/types/**/*.ts",
|
||||||
".next/dev/types/**/*.ts"
|
".next/dev/types/**/*.ts"
|
||||||
],
|
],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules", "packages"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user