Skip to content

feat: support Model Context Protocol in VSCode extension #2132

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 69 additions & 1 deletion packages/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,36 @@
"title": "Unsync preview window with editor cursor",
"icon": "$(lock)",
"enablement": "slidev:preview:sync"
},
{
"command": "slidev.mcp.start",
"category": "Slidev",
"title": "Start MCP server",
"icon": "$(play)"
},
{
"command": "slidev.mcp.stop",
"category": "Slidev",
"title": "Stop MCP server",
"icon": "$(stop-circle)"
},
{
"command": "slidev.open-mcp-settings",
"category": "Slidev",
"title": "Open MCP settings",
"icon": "$(settings)"
},
{
"command": "slidev.mcp.copy-tool",
"title": "Copy MCP Tool Name",
"category": "Slidev",
"icon": "$(copy)"
},
{
"command": "slidev.mcp.copy-url",
"title": "Copy MCP URL",
"category": "Slidev",
"icon": "$(link)"
}
],
"menus": {
Expand Down Expand Up @@ -319,6 +349,21 @@
"command": "slidev.disable-preview-sync",
"when": "view =~ /slidev-preview/ && slidev:preview:sync",
"group": "navigation@7"
},
{
"command": "slidev.mcp.stop",
"when": "view =~ /slidev-mcp-tree/ && slidev:mcp:status",
"group": "navigation@1"
},
{
"command": "slidev.mcp.start",
"when": "view =~ /slidev-mcp-tree/ && !slidev:mcp:status",
"group": "navigation@1"
},
{
"command": "slidev.mcp.copy-url",
"when": "view =~ /slidev-mcp-tree/ && slidev:mcp:status",
"group": "navigation@2"
}
],
"view/item/context": [
Expand Down Expand Up @@ -391,6 +436,17 @@
"scope": "window",
"description": "The command to start Slidev dev server. See https://sli.dev/features/vscode-extension#dev-command",
"default": "npm exec -c 'slidev ${args}'"
},
"slidev.mcp.port": {
"type": "number",
"scope": "window",
"description": "The port of Model Context Protocol (MCP) server.",
"default": 4000
},
"slidev.mcp.ide": {
"type": "string",
"scope": "window",
"description": "The IDE name to be used in MCP server."
}
}
},
Expand Down Expand Up @@ -426,13 +482,23 @@
"visibility": "visible",
"initialSize": 3,
"when": "slidev:enabled"
},
{
"id": "slidev-mcp-tree",
"name": "MCP Server",
"visibility": "collapsed",
"when": "slidev:enabled"
}
]
},
"viewsWelcome": [
{
"view": "slidev-slides-tree",
"contents": "No active slides entry.\n[Choose one](command:slidev.choose-entry)"
},
{
"view": "slidev-mcp-tree",
"contents": "MCP server is not running.\n[Start MCP server](command:slidev.mcp.start)\n[Open MCP settings](command:slidev.open-mcp-settings)"
}
]
},
Expand All @@ -452,13 +518,15 @@
"@types/vscode": "^1.89.0",
"@volar/language-server": "catalog:",
"@volar/vscode": "catalog:",
"fastmcp": "^1.20.5",
"get-port-please": "catalog:",
"mlly": "catalog:",
"ovsx": "catalog:",
"prettier": "catalog:",
"reactive-vscode": "catalog:",
"tm-grammars": "catalog:",
"volar-service-prettier": "catalog:",
"volar-service-yaml": "catalog:"
"volar-service-yaml": "catalog:",
"zod": "^3.24.2"
}
}
29 changes: 28 additions & 1 deletion packages/vscode/src/commands.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { relative } from 'node:path'
import { slash } from '@antfu/utils'
import { useCommand } from 'reactive-vscode'
import { Position, Range, Selection, TextEditorRevealType, Uri, window, workspace } from 'vscode'
import { commands, env, Position, Range, Selection, TextEditorRevealType, Uri, window, workspace } from 'vscode'
import { useDevServer } from './composables/useDevServer'
import { useEditingSlideSource } from './composables/useEditingSlideSource'
import { useFocusedSlideNo } from './composables/useFocusedSlideNo'
import { useMcpServer } from './composables/useMcpServer'
import { configuredPort, forceEnabled, include, previewSync } from './configs'
import { activeEntry, activeProject, activeSlidevData, addProject, projects, rescanProjects } from './projects'
import { findPossibleEntries } from './utils/findPossibleEntries'
Expand Down Expand Up @@ -155,4 +156,30 @@ export function useCommands() {

useCommand('slidev.enable-preview-sync', () => (previewSync.value = true))
useCommand('slidev.disable-preview-sync', () => (previewSync.value = false))
useCommand('slidev.mcp.start', () => {
const { start } = useMcpServer()
start()
})
useCommand('slidev.mcp.stop', () => {
const { stop } = useMcpServer()
stop()
})
useCommand('slidev.open-mcp-settings', () => {
return commands.executeCommand('workbench.action.openSettings', 'slidev.mcp')
})
useCommand('slidev.mcp.copy-tool', (tool: string) => {
env.clipboard.writeText(tool).then(() => {
window.showInformationMessage(`Tool Name "${tool}" copied to clipboard.`)
}, (error) => {
window.showErrorMessage(`Copy Error: ${error}`)
})
})
useCommand('slidev.mcp.copy-url', () => {
const { url } = useMcpServer()
env.clipboard.writeText(url.value).then(() => {
window.showInformationMessage(`URL "${url.value}" copied to clipboard.`)
}, (error) => {
window.showErrorMessage(`Copy Error: ${error}`)
})
})
}
78 changes: 78 additions & 0 deletions packages/vscode/src/composables/useMcpServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type { FastMCP } from 'fastmcp'
import { computed, createSingletonComposable, reactive, useVscodeContext } from 'reactive-vscode'
import { window, workspace } from 'vscode'
import { mcpIDE, mcpPort, mcpUrl } from '../configs'
import { createMcpServerDefault } from '../utils/createMcpServer'
import { tools } from '../utils/createMcpTools'
import { updateIDEMcpConfig } from '../utils/updateIDEMcpConfig'
import { logger } from '../views/logger'

export const useMcpServer = createSingletonComposable(() => {
const endpoint = '/sse'
const state = reactive({
status: false,
tools,
})
useVscodeContext('slidev:mcp:status', () => state.status)

let serverInstance: FastMCP | null = null

/**
* Start MCP service
*/
async function start() {
if (state.status) {
window.showInformationMessage('MCP Server is already running.')
return
}
try {
if (!serverInstance) {
serverInstance = await createMcpServerDefault({ tools })
}
serverInstance.start({
transportType: 'sse',
sse: {
endpoint,
port: mcpPort.value,
},
})
state.status = true
if (!!mcpIDE.value && workspace.workspaceFolders && workspace.workspaceFolders.length > 0) {
const rootPath = workspace.workspaceFolders[0].uri.fsPath
await updateIDEMcpConfig(
rootPath,
`${mcpUrl.value}${endpoint}`,
mcpIDE.value,
)
}
window.showInformationMessage(`Slidev MCP Server is started, url: ${mcpUrl.value}${endpoint}`)
}
catch (error) {
window.showErrorMessage(`MCP Server Error: ${error}`)
state.status = false
}
}

/**
* Stop MCP server
*/
function stop() {
if (state.status && serverInstance) {
serverInstance.stop()
state.status = false
window.showInformationMessage('Slidev MCP Server is stopped.')
logger.info('Slidev MCP Server is stopped.')
}
else {
window.showInformationMessage('Slidev MCP Server is not running.')
}
}

return {
state,
server: computed(() => serverInstance),
url: computed(() => `${mcpUrl.value}${endpoint}`),
start,
stop,
}
})
10 changes: 9 additions & 1 deletion packages/vscode/src/configs.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ConfigType } from 'reactive-vscode'
import { defineConfigs, ref } from 'reactive-vscode'
import { computed, defineConfigs, ref } from 'reactive-vscode'

export const {
'force-enabled': forceEnabled,
Expand All @@ -9,6 +9,8 @@ export const {
include,
exclude,
'dev-command': devCommand,
'mcp.port': mcpPort,
'mcp.ide': mcpIDE,
} = defineConfigs('slidev', {
'force-enabled': Boolean,
'port': Number,
Expand All @@ -17,7 +19,13 @@ export const {
'include': Object as ConfigType<string[]>,
'exclude': String,
'dev-command': String,
'mcp.port': Number,
'mcp.ide': Object as ConfigType<'vscode' | 'cursor' | undefined>,
})

export const configuredPort = ref(configuredPortInitial)
export const previewSync = ref(previewSyncInitial)
export const mcpUrl = computed(() => {
const port = mcpPort.value
return `http://localhost:${port}`
})
2 changes: 2 additions & 0 deletions packages/vscode/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { activeEntry, useProjects } from './projects'
import { useAnnotations } from './views/annotations'
import { useFoldings } from './views/foldings'
import { logger } from './views/logger'
import { useMcpTreeView } from './views/mcpTreeView'
import { usePreviewWebview } from './views/previewWebview'
import { useProjectsTree } from './views/projectsTree'
import { useSlidesTree } from './views/slidesTree'
Expand All @@ -22,6 +23,7 @@ const { activate, deactivate } = defineExtension(() => {
usePreviewWebview()
useAnnotations()
useFoldings()
useMcpTreeView()

// language server
const labsInfo = useLanguageClient()
Expand Down
51 changes: 51 additions & 0 deletions packages/vscode/src/utils/createMcpServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { FastMCP, Tool } from 'fastmcp'
import { window } from 'vscode'
import { logger } from '../views/logger'

// Fastmcp does not support ESM, so we need to use dynamic import
let FastMCPModule: any = null

/**
* Create a default MCP server
* @returns {FastMCP}
*/
export async function createMcpServerDefault({ tools }: { tools: Tool<any, any>[] }): Promise<FastMCP> {
if (!FastMCPModule) {
try {
FastMCPModule = await import('fastmcp')
}
catch (error) {
logger.error('Failed to load FastMCP module:', error)
window.showErrorMessage(`Failed to load MCP module: ${error}`)
throw error
}
}
try {
const { FastMCP } = FastMCPModule
const server = new FastMCP(
{
name: 'slidev',
version: '1.0.0',
},
)

for (const tool of tools) {
server.addTool(tool)
}

server.on('connect', (event: any) => {
logger.info('Client connected:', event.session)
})

server.on('disconnect', (event: any) => {
logger.info('Client disconnected:', event.session)
})

return server
}
catch (error) {
logger.error('Failed to create MCP server:', error)
window.showErrorMessage(`Failed to create MCP server: ${error}`)
throw error
}
}
Loading