Skip to content

Commit f28250e

Browse files
committedMar 15, 2025
Add model context server
1 parent c1dfea1 commit f28250e

File tree

8 files changed

+647
-28
lines changed

8 files changed

+647
-28
lines changed
 

‎crates/zed-plugin-gem/extension.toml

+3
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,6 @@ snippets = "./snippets/typescript.json"
1111
name = "Gem language server"
1212
languages = ["TypeScript", "TSX", "JavaScript", "JSDoc"]
1313
language_ids = { "TypeScript" = "typescript", "TSX" = "typescriptreact", "JavaScript" = "javascript" }
14+
15+
[context_servers.gem]
16+
name = "Gem Context Server"

‎crates/zed-plugin-gem/src/lib.rs

+39-15
Original file line numberDiff line numberDiff line change
@@ -7,59 +7,62 @@
77
use std::{env, fs};
88

99
use zed::settings::LspSettings;
10-
use zed_extension_api::{self as zed, LanguageServerId, Result};
10+
use zed_extension_api::{self as zed, Command, ContextServerId, LanguageServerId, Project, Result};
1111

12-
const NPM_PKG_NAME: &str = "vscode-gem-languageservice";
12+
const LS_PKG_NAME: &str = "vscode-gem-languageservice";
1313
const LS_BIN_PATH: &str = "node_modules/.bin/vscode-gem-languageservice";
1414

15+
const MC_PKG_NAME: &str = "gem-context-server";
16+
const MC_BIN_PATH: &str = "node_modules/.bin/gem-context-server";
17+
1518
#[derive(Default)]
1619
struct GemExtension {
17-
did_find_server: bool,
20+
ls_server_find: bool,
1821
}
1922

2023
impl GemExtension {
21-
fn server_exists(&self) -> bool {
24+
fn ls_exists(&self) -> bool {
2225
fs::metadata(LS_BIN_PATH).is_ok_and(|stat| stat.is_file())
2326
}
2427

25-
fn server_script_path(&mut self, language_server_id: &zed::LanguageServerId) -> Result<String> {
26-
let server_exists = self.server_exists();
27-
if self.did_find_server && server_exists {
28+
fn ls_path(&mut self, language_server_id: &LanguageServerId) -> Result<String> {
29+
let server_exists = self.ls_exists();
30+
if self.ls_server_find && server_exists {
2831
return Ok(LS_BIN_PATH.to_string());
2932
}
3033

3134
zed::set_language_server_installation_status(
3235
language_server_id,
3336
&zed::LanguageServerInstallationStatus::CheckingForUpdate,
3437
);
35-
let version = zed::npm_package_latest_version(NPM_PKG_NAME)?;
38+
let version = zed::npm_package_latest_version(LS_PKG_NAME)?;
3639

3740
if !server_exists
38-
|| zed::npm_package_installed_version(NPM_PKG_NAME)?.as_ref() != Some(&version)
41+
|| zed::npm_package_installed_version(LS_PKG_NAME)?.as_ref() != Some(&version)
3942
{
4043
zed::set_language_server_installation_status(
4144
language_server_id,
4245
&zed::LanguageServerInstallationStatus::Downloading,
4346
);
44-
let result = zed::npm_install_package(NPM_PKG_NAME, &version);
47+
let result = zed::npm_install_package(LS_PKG_NAME, &version);
4548
match result {
4649
Ok(()) => {
47-
if !self.server_exists() {
50+
if !self.ls_exists() {
4851
Err(format!(
49-
"installed package '{NPM_PKG_NAME}' did not contain expected path \
52+
"installed package '{LS_PKG_NAME}' did not contain expected path \
5053
'{LS_BIN_PATH}'",
5154
))?;
5255
}
5356
}
5457
Err(error) => {
55-
if !self.server_exists() {
58+
if !self.ls_exists() {
5659
Err(error)?;
5760
}
5861
}
5962
}
6063
}
6164

62-
self.did_find_server = true;
65+
self.ls_server_find = true;
6366
Ok(LS_BIN_PATH.to_string())
6467
}
6568
}
@@ -69,12 +72,33 @@ impl zed::Extension for GemExtension {
6972
Self::default()
7073
}
7174

75+
fn context_server_command(
76+
&mut self,
77+
_context_server_id: &ContextServerId,
78+
_project: &Project,
79+
) -> Result<Command> {
80+
let version = zed::npm_package_latest_version(MC_PKG_NAME)?;
81+
if zed::npm_package_installed_version(MC_PKG_NAME)?.as_ref() != Some(&version) {
82+
zed::npm_install_package(MC_PKG_NAME, &version)?;
83+
}
84+
85+
Ok(Command {
86+
command: zed::node_binary_path()?,
87+
args: vec![env::current_dir()
88+
.unwrap()
89+
.join(MC_BIN_PATH)
90+
.to_string_lossy()
91+
.to_string()],
92+
env: Default::default(),
93+
})
94+
}
95+
7296
fn language_server_command(
7397
&mut self,
7498
language_server_id: &LanguageServerId,
7599
_worktree: &zed::Worktree,
76100
) -> Result<zed::Command> {
77-
let server_path = self.server_script_path(language_server_id)?;
101+
let server_path = self.ls_path(language_server_id)?;
78102
Ok(zed::Command {
79103
command: zed::node_binary_path()?,
80104
args: vec![

‎cspell.json

+1
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
"Microtask",
8585
"minisearch",
8686
"minmax",
87+
"modelcontextprotocol",
8788
"mooncake",
8889
"mtapp",
8990
"multiselectable",

‎packages/context-server/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Gem model context server
2+
3+
Use for Zed & Cursor

‎packages/context-server/package.json

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"name": "gem-context-server",
3+
"version": "0.0.1",
4+
"description": "Model context server for Gem",
5+
"keywords": [
6+
"gem",
7+
"MCP"
8+
],
9+
"module": "src/index.ts",
10+
"main": "src/index.ts",
11+
"bin": "dist/index.js",
12+
"files": [
13+
"/dist/"
14+
],
15+
"scripts": {
16+
"prepublishOnly": "esbuild ./src/index.ts --outdir=./dist --platform=node --sourcemap --bundle --packages=external"
17+
},
18+
"dependencies": {
19+
"adm-zip": "^0.5.16",
20+
"@modelcontextprotocol/sdk": "^1.6.1"
21+
},
22+
"devDependencies": {
23+
"@gemjs/config": "^2.1.0",
24+
"@types/adm-zip": "^0.5.7"
25+
},
26+
"author": "mantou132",
27+
"license": "MIT",
28+
"repository": {
29+
"type": "git",
30+
"url": "git+https://github.com/mantou132/gem.git",
31+
"directory": "packages/context-server"
32+
},
33+
"bugs": {
34+
"url": "https://github.com/mantou132/gem/issues"
35+
},
36+
"homepage": "https://github.com/mantou132/gem#readme"
37+
}

‎packages/context-server/src/index.ts

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3+
import AdmZip from 'adm-zip';
4+
5+
async function downloadDocs() {
6+
const res = await fetch('https://github.com/mantou132/gem/archive/refs/heads/main.zip');
7+
const buf = await res.arrayBuffer();
8+
const zip = new AdmZip(Buffer.from(buf));
9+
const zipEntries = zip.getEntries();
10+
return zipEntries
11+
.filter((entry) => entry.entryName.includes('packages/gem/docs/en') && entry.entryName.endsWith('.md'))
12+
.map((e) => ({
13+
entryName: e.entryName,
14+
content: zip.readAsText(e),
15+
}));
16+
}
17+
18+
const server = new McpServer({
19+
name: 'Gem',
20+
version: '0.0.1',
21+
});
22+
23+
let docsCache: Awaited<ReturnType<typeof downloadDocs>> | undefined;
24+
server.resource('docs', 'docs://all', async () => {
25+
const docs = docsCache || (docsCache = await downloadDocs());
26+
return {
27+
contents: docs.map(({ entryName, content }) => ({ uri: `docs://${entryName}`, text: content })),
28+
};
29+
});
30+
31+
const transport = new StdioServerTransport();
32+
server.connect(transport);

‎packages/context-server/tsconfig.json

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"extends": "@gemjs/config/tsconfig",
3+
"include": ["src"],
4+
"exclude": []
5+
}

‎pnpm-lock.yaml

+527-13
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
Please sign in to comment.