@opticlm/connector 2.4.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -7
- package/dist/pipe/index.d.ts +8 -4
- package/dist/pipe/index.js +5 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -340,24 +340,31 @@ install(server, definitionProvider, {
|
|
|
340
340
|
|
|
341
341
|
When the MCP server runs in a separate process from the IDE plugin (e.g., spawned via stdio transport), the Pipe IPC layer lets the two communicate over a named pipe.
|
|
342
342
|
|
|
343
|
-
**IDE plugin side** — expose providers:
|
|
343
|
+
**IDE plugin side** — expose providers via a factory that receives client context:
|
|
344
344
|
|
|
345
345
|
```typescript
|
|
346
346
|
import { servePipe } from '@opticlm/connector/pipe'
|
|
347
347
|
|
|
348
348
|
const server = await servePipe({
|
|
349
349
|
pipeName: 'my-ide-lsp',
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
350
|
+
createProviders(context) {
|
|
351
|
+
// context is whatever the client sent at connect time
|
|
352
|
+
const { workspacePath } = context as { workspacePath: string }
|
|
353
|
+
return {
|
|
354
|
+
fileAccess: new MyFileAccess(workspacePath),
|
|
355
|
+
definition: new MyDefinitionProvider(workspacePath),
|
|
356
|
+
// Add only the providers your IDE supports
|
|
357
|
+
}
|
|
358
|
+
},
|
|
354
359
|
})
|
|
355
360
|
// server.pipePath — the resolved pipe path
|
|
356
361
|
// server.connectionCount — number of connected clients
|
|
357
362
|
// await server.close() — shut down
|
|
358
363
|
```
|
|
359
364
|
|
|
360
|
-
|
|
365
|
+
The factory is called once per incoming connection, so providers can be tailored to each client. It may return a plain object or a `Promise` for async initialization.
|
|
366
|
+
|
|
367
|
+
**MCP server side** — connect with context and install proxy providers:
|
|
361
368
|
|
|
362
369
|
```typescript
|
|
363
370
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
@@ -367,6 +374,7 @@ import { install } from '@opticlm/connector/mcp'
|
|
|
367
374
|
const conn = await connectPipe({
|
|
368
375
|
pipeName: 'my-ide-lsp',
|
|
369
376
|
connectTimeout: 5000, // optional, default 5000ms
|
|
377
|
+
context: { workspacePath: '/path/to/project' }, // sent to createProviders
|
|
370
378
|
})
|
|
371
379
|
|
|
372
380
|
// conn exposes proxy providers as named fields:
|
|
@@ -385,7 +393,29 @@ if (conn.definition && conn.fileAccess)
|
|
|
385
393
|
conn.disconnect()
|
|
386
394
|
```
|
|
387
395
|
|
|
388
|
-
The handshake automatically discovers which providers the server exposes and builds typed proxies.
|
|
396
|
+
The handshake automatically discovers which providers the server exposes and builds typed proxies. Multiple clients can connect to the same pipe simultaneously, each with their own provider instances.
|
|
397
|
+
|
|
398
|
+
**Broadcasting to multiple clients** — since each connection gets its own providers, use a shared subscriber set for push notifications that should reach all clients:
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
const diagnosticsSubscribers = new Set<(uri: string) => void>()
|
|
402
|
+
yourIDE.onDiagnosticsChanged((uri) => {
|
|
403
|
+
for (const cb of diagnosticsSubscribers) cb(uri)
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
await servePipe({
|
|
407
|
+
pipeName: 'my-ide-lsp',
|
|
408
|
+
createProviders(context) {
|
|
409
|
+
return {
|
|
410
|
+
fileAccess: { /* ... */ },
|
|
411
|
+
diagnostics: {
|
|
412
|
+
provideDiagnostics: (uri) => yourIDE.getDiagnostics(uri),
|
|
413
|
+
onDiagnosticsChanged: (cb) => diagnosticsSubscribers.add(cb),
|
|
414
|
+
},
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
})
|
|
418
|
+
```
|
|
389
419
|
|
|
390
420
|
## LSP Client (Built-in)
|
|
391
421
|
|
package/dist/pipe/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { F as FileAccessProvider, E as EditProvider, D as DefinitionProvider, R
|
|
|
3
3
|
interface connectPipeOptions {
|
|
4
4
|
pipeName: string;
|
|
5
5
|
connectTimeout?: number;
|
|
6
|
+
context?: unknown;
|
|
6
7
|
}
|
|
7
8
|
interface PipeConnection {
|
|
8
9
|
readonly fileAccess?: FileAccessProvider;
|
|
@@ -20,8 +21,7 @@ interface PipeConnection {
|
|
|
20
21
|
}
|
|
21
22
|
declare function connectPipe(options: connectPipeOptions): Promise<PipeConnection>;
|
|
22
23
|
|
|
23
|
-
interface
|
|
24
|
-
pipeName: string;
|
|
24
|
+
interface ProviderSet {
|
|
25
25
|
fileAccess: FileAccessProvider;
|
|
26
26
|
edit?: EditProvider;
|
|
27
27
|
definition?: DefinitionProvider;
|
|
@@ -33,11 +33,15 @@ interface servePipeOptions {
|
|
|
33
33
|
graph?: GraphProvider;
|
|
34
34
|
frontmatter?: FrontmatterProvider;
|
|
35
35
|
}
|
|
36
|
+
interface ServePipeOptions {
|
|
37
|
+
pipeName: string;
|
|
38
|
+
createProviders: (context: unknown) => ProviderSet | Promise<ProviderSet>;
|
|
39
|
+
}
|
|
36
40
|
interface PipeServer {
|
|
37
41
|
readonly pipePath: string;
|
|
38
42
|
readonly connectionCount: number;
|
|
39
43
|
close(): Promise<void>;
|
|
40
44
|
}
|
|
41
|
-
declare function servePipe(options:
|
|
45
|
+
declare function servePipe(options: ServePipeOptions): Promise<PipeServer>;
|
|
42
46
|
|
|
43
|
-
export { type PipeConnection, type PipeServer, connectPipe, type connectPipeOptions, servePipe, type servePipeOptions };
|
|
47
|
+
export { type PipeConnection, type PipeServer, type ProviderSet, connectPipe, type connectPipeOptions, servePipe, type ServePipeOptions as servePipeOptions };
|
package/dist/pipe/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {connect,createServer}from'net';import {unlinkSync}from'fs';var
|
|
2
|
-
`);})}sendNotification(e,r){if(this.destroyed)return;let
|
|
1
|
+
import {connect,createServer}from'net';import {unlinkSync}from'fs';var y=class{socket;requestHandler;notificationHandler;nextId=1;pending=new Map;buffer="";destroyed=false;constructor(e,r){this.socket=e,this.requestHandler=r?.onRequest,this.notificationHandler=r?.onNotification,e.on("data",i=>this.handleData(i)),e.on("close",()=>this.rejectAll(new Error("Connection closed"))),e.on("error",i=>this.rejectAll(i));}sendRequest(e,r){return this.destroyed?Promise.reject(new Error("Transport destroyed")):new Promise((i,a)=>{let s=this.nextId++;this.pending.set(s,{resolve:i,reject:a});let t={type:"request",id:s,method:e,params:r};this.socket.write(`${JSON.stringify(t)}
|
|
2
|
+
`);})}sendNotification(e,r){if(this.destroyed)return;let i={type:"notification",method:e,params:r};this.socket.write(`${JSON.stringify(i)}
|
|
3
3
|
`);}destroy(){this.destroyed||(this.destroyed=true,this.rejectAll(new Error("Transport destroyed")),this.socket.destroy());}handleData(e){this.buffer+=e.toString();let r=this.buffer.split(`
|
|
4
|
-
`);this.buffer=r.pop()??"";for(let
|
|
5
|
-
`);}).catch(r=>{if(this.destroyed)return;let
|
|
6
|
-
`);});break}case "notification":{this.notificationHandler?.(e.method,e.params);break}}}rejectAll(e){for(let[,r]of this.pending)r.reject(e);this.pending.clear();}};function
|
|
4
|
+
`);this.buffer=r.pop()??"";for(let i of r){let a=i.replace(/\r$/,"");if(a)try{let s=JSON.parse(a);this.dispatch(s);}catch{}}}dispatch(e){switch(e.type){case "response":{let r=this.pending.get(e.id);r&&(this.pending.delete(e.id),e.error?r.reject(new Error(e.error.message)):r.resolve(e.result));break}case "request":{this.requestHandler&&this.requestHandler(e.method,e.params).then(r=>{if(this.destroyed)return;let i={type:"response",id:e.id,result:r};this.socket.write(`${JSON.stringify(i)}
|
|
5
|
+
`);}).catch(r=>{if(this.destroyed)return;let i={type:"response",id:e.id,error:{message:r instanceof Error?r.message:String(r)}};this.socket.write(`${JSON.stringify(i)}
|
|
6
|
+
`);});break}case "notification":{this.notificationHandler?.(e.method,e.params);break}}}rejectAll(e){for(let[,r]of this.pending)r.reject(e);this.pending.clear();}};function b(f){return process.platform==="win32"?`\\\\.\\pipe\\${f}`:`/tmp/${f}.sock`}var F=[{providerKey:"fileAccess",methods:["readFile","readDirectory"]},{providerKey:"edit",methods:["applyEdits"]},{providerKey:"definition",methods:["provideDefinition"]},{providerKey:"references",methods:["provideReferences"]},{providerKey:"hierarchy",methods:["provideCallHierarchy"]},{providerKey:"diagnostics",methods:["provideDiagnostics","getWorkspaceDiagnostics"]},{providerKey:"outline",methods:["provideDocumentSymbols"]},{providerKey:"globalFind",methods:["globalFind"]},{providerKey:"graph",methods:["getLinkStructure","resolveOutlinks","resolveBacklinks","addLink"]},{providerKey:"frontmatter",methods:["getFrontmatterStructure","getFrontmatter","setFrontmatter"]}];function E(f){let{pipeName:e,connectTimeout:r=5e3}=f,i=b(e);return new Promise((a,s)=>{let t=connect(i),n=false,u=setTimeout(()=>{n||(n=true,t.destroy(),s(new Error(`Connection timeout after ${r}ms`)));},r);t.on("error",c=>{n||(n=true,clearTimeout(u),s(c));}),t.on("connect",()=>{if(clearTimeout(u),n)return;let c,P,g=new y(t,{onNotification:(o,v)=>{o==="onDiagnosticsChanged"&&c&&c(v[0]),o==="onFileChanged"&&P&&P(v[0]);}});g.sendRequest("_handshake",[f.context]).then(o=>{if(n)return;let p=o.methods,d={};for(let{providerKey:l,methods:k}of F){let w=k.filter(h=>p.includes(`${l}.${h}`));if(w.length===0)continue;let m={};for(let h of w)m[h]=(...S)=>g.sendRequest(`${l}.${h}`,S);l==="diagnostics"&&p.includes("onDiagnosticsChanged")&&(m.onDiagnosticsChanged=h=>{c=h;}),l==="fileAccess"&&p.includes("onFileChanged")&&(m.onFileChanged=h=>{P=h;}),d[l]=m;}n=true,a({fileAccess:d.fileAccess,edit:d.edit,definition:d.definition,references:d.references,hierarchy:d.hierarchy,diagnostics:d.diagnostics,outline:d.outline,globalFind:d.globalFind,graph:d.graph,frontmatter:d.frontmatter,availableMethods:p,disconnect(){g.destroy();}});}).catch(o=>{n||(n=true,g.destroy(),s(o instanceof Error?o:new Error(String(o))));});});})}function C(f){let{pipeName:e}=f,r=b(e),i=new Set,a=createServer(s=>{let t=null,n=new y(s,{onRequest:async(u,c)=>{if(u==="_handshake"){let g=c[0],o=await f.createProviders(g);t=new Map;for(let{providerKey:p,methods:d}of F){let l=o[p];if(l)for(let k of d){let w=l[k];typeof w=="function"&&t.set(`${p}.${k}`,(...m)=>w.apply(l,m));}}let v=[...t.keys()];return o.diagnostics?.onDiagnosticsChanged&&(o.diagnostics.onDiagnosticsChanged(p=>{n.sendNotification("onDiagnosticsChanged",[p]);}),v.push("onDiagnosticsChanged")),o.fileAccess.onFileChanged&&(o.fileAccess.onFileChanged(p=>{n.sendNotification("onFileChanged",[p]);}),v.push("onFileChanged")),{methods:v}}if(!t)throw new Error("Handshake not completed");let P=t.get(u);if(!P)throw new Error(`Unknown method: ${u}`);return P(...c)}});i.add(n),s.on("close",()=>i.delete(n));});return new Promise((s,t)=>{if(process.platform!=="win32")try{unlinkSync(r);}catch{}a.on("error",t),a.listen(r,()=>{a.removeListener("error",t),s({get pipePath(){return r},get connectionCount(){return i.size},async close(){for(let n of i)n.destroy();if(i.clear(),await new Promise((n,u)=>{a.close(c=>{c?u(c):n();});}),process.platform!=="win32")try{unlinkSync(r);}catch{}}});});})}export{E as connectPipe,C as servePipe};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opticlm/connector",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "Provides an abstract interface that allows LLMs to connect to fact sources such as LSPs, code diagnostics, symbol definitions/references, links, and frontmatter",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "dist/index.d.ts",
|