Skip to content

Commit cf7fb65

Browse files
BrainSlugs83Copilot
andcommitted
feat: zero-config auto-port assignment via FNV-1a hash
Each user gets a deterministic port based on their OS username (FNV-1a hash with ROR by name length, 12-bit range 31337-35432). Multi-user setups now work without any manual VECTOR_MEMORY_PORT configuration. The env var remains as an escape hatch for the rare hash collision case. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent f7d16aa commit cf7fb65

File tree

5 files changed

+44
-25
lines changed

5 files changed

+44
-25
lines changed

README.md

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ copilot.exe ──STDIO──▶ index.js (proxy) ──HTTP──▶ vector-mem
1515
```
1616

1717
- **index.js** — Thin STDIO MCP proxy. One per copilot instance. Checks if the HTTP server is running, launches it if not, then ferries tool calls over HTTP.
18-
- **vector-memory-server.js** — Singleton HTTP server on `localhost:31337`. Owns the embedding model (one copy in memory), SQLite vector DB, and background indexing.
18+
- **vector-memory-server.js** — Singleton HTTP server. Owns the embedding model (one copy in memory), SQLite vector DB, and background indexing. Port is auto-assigned per user via a deterministic hash of the username.
1919
- **embed-worker.js** — Worker thread that loads the ONNX model and handles embedding inference off the main thread.
2020
- **lib.js** — Pure logic extracted for testability: filtering, dedup, post-processing, process detection.
2121

@@ -67,10 +67,10 @@ Add to `~/.copilot/mcp-config.json`:
6767

6868
| Variable | Default | Description |
6969
|---|---|---|
70-
| `VECTOR_MEMORY_PORT` | `31337` | HTTP port for the singleton server. Change this to run independent instances per user. |
70+
| `VECTOR_MEMORY_PORT` | *(auto)* | HTTP port for the singleton server. By default, a deterministic port is computed from your OS username (FNV-1a hash, range 31337–35432). Only set this if two users happen to collide. |
7171
| `VECTOR_MEMORY_IDLE_TIMEOUT` | `5` | Minutes of inactivity before the server shuts down. `0` or negative = never shut down. |
7272

73-
Set these in the `env` block of `mcp-config.json`:
73+
Set these in the `env` block of `mcp-config.json` (only if needed):
7474

7575
```json
7676
{
@@ -79,19 +79,24 @@ Set these in the `env` block of `mcp-config.json`:
7979
"command": "node",
8080
"args": ["C:/Users/<you>/.copilot/mcp-servers/vector-memory/index.js"],
8181
"env": {
82-
"VECTOR_MEMORY_PORT": "31338",
8382
"VECTOR_MEMORY_IDLE_TIMEOUT": "10"
8483
}
8584
}
8685
}
8786
}
8887
```
8988

89+
#### Auto-port assignment
90+
91+
Each user automatically gets a deterministic port based on their OS username (FNV-1a hash with rotation, range 31337–35432). This means multi-user setups work **zero-config** — no manual port assignment needed.
92+
93+
In the rare case of a hash collision (two usernames mapping to the same port), the server detects the conflict at startup and tells the affected user to set `VECTOR_MEMORY_PORT` manually.
94+
9095
#### Multi-user setup
9196

92-
On a shared machine, each user needs their own port to keep session history isolated (the vector index lives in each user's `~/.copilot/`). Point both configs at the same codebase:
97+
On a shared machine, each user's server runs on their auto-assigned port. Just point both configs at the same codebase — no `env` block needed:
9398

94-
**User A** (`~/.copilot/mcp-config.json`) — uses default port 31337:
99+
**User A** (`~/.copilot/mcp-config.json`):
95100
```json
96101
{
97102
"mcpServers": {
@@ -103,16 +108,13 @@ On a shared machine, each user needs their own port to keep session history isol
103108
}
104109
```
105110

106-
**User B** (`~/.copilot/mcp-config.json`) — uses port 31338:
111+
**User B** (`~/.copilot/mcp-config.json`) — same config, different port automatically:
107112
```json
108113
{
109114
"mcpServers": {
110115
"vector-memory": {
111116
"command": "node",
112-
"args": ["D:/shared/vector-memory/index.js"],
113-
"env": {
114-
"VECTOR_MEMORY_PORT": "31338"
115-
}
117+
"args": ["D:/shared/vector-memory/index.js"]
116118
}
117119
}
118120
}
@@ -161,11 +163,11 @@ node --test --experimental-test-coverage test.js
161163
# Start server directly (normally done by the proxy)
162164
node vector-memory-server.js
163165

164-
# Check if running
165-
curl -X POST http://127.0.0.1:31337/ping -d "{}"
166+
# Check if running (port varies per user — see startup log)
167+
curl -X POST http://127.0.0.1:<PORT>/ping -d "{}"
166168

167169
# Search directly
168-
curl -X POST http://127.0.0.1:31337/search \
170+
curl -X POST http://127.0.0.1:<PORT>/search \
169171
-H "Content-Type: application/json" \
170172
-d '{"query":"what did I work on yesterday","limit":5}'
171173

@@ -186,11 +188,11 @@ cat ~/.copilot/vector-memory.pid
186188

187189
## Troubleshooting
188190

189-
### Port owned by another user
191+
### Port collision with another user
190192

191-
**Error:** `Port 31337 is owned by user "X" (expected "Y")`
193+
**Error:** `Port 31796 is owned by user "X" (expected "Y")`
192194

193-
Two users on the same machine are pointing at the same port. Each user needs a unique port. Edit `~/.copilot/mcp-config.json` and add a `VECTOR_MEMORY_PORT` env var:
195+
Two usernames hashed to the same port (rare — FNV-1a produces very few collisions). One user needs to set a manual override. Edit `~/.copilot/mcp-config.json` and add a `VECTOR_MEMORY_PORT` env var:
194196

195197
```json
196198
{
@@ -210,9 +212,9 @@ See [Multi-user setup](#multi-user-setup) for full details.
210212

211213
### Port occupied by another service
212214

213-
**Error:** `Vector memory server failed to start — port 31337 may be in use by another service`
215+
**Error:** `Vector memory server failed to start — port XXXXX may be in use by another service`
214216

215-
Something else is listening on the default port. Pick a different port using the same `VECTOR_MEMORY_PORT` env var as above.
217+
Something else is listening on your auto-assigned port. Pick a different port using the `VECTOR_MEMORY_PORT` env var as above.
216218

217219
To check what's on the port:
218220
```bash

index.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ import { userInfo } from "os";
1111

1212
const __dirname = dirname(fileURLToPath(import.meta.url));
1313
const COPILOT_DIR = join(homedir(), ".copilot");
14-
const PORT = parseInt(process.env.VECTOR_MEMORY_PORT || "31337", 10);
14+
const EXPECTED_USER = userInfo().username;
15+
const PKG = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf-8"));
16+
17+
import { userPort } from "./lib.js";
18+
const PORT = parseInt(process.env.VECTOR_MEMORY_PORT || String(userPort(EXPECTED_USER)), 10);
1519
const SERVER_URL = `http://127.0.0.1:${PORT}`;
1620
const PID_FILE = join(COPILOT_DIR, "vector-memory.pid");
17-
const PKG = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf-8"));
18-
const EXPECTED_USER = userInfo().username;
1921

2022
// --- Check if server is running ---
2123

lib.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,21 @@
33
export const DIMS = 384;
44
export const MIN_SCORE = 0.25;
55
export const JITTER = 0.05;
6+
export const BASE_PORT = 31337;
7+
8+
/** FNV-1a hash with ROR by length, truncated to 12 bits. Deterministic port per username. */
9+
export function userPort(username) {
10+
const str = username.toLowerCase();
11+
let h = 0x811c9dc5;
12+
for (let i = 0; i < str.length; i++) {
13+
h ^= str.charCodeAt(i);
14+
h = Math.imul(h, 0x01000193);
15+
}
16+
h = h >>> 0;
17+
const rot = str.length % 32;
18+
h = ((h >>> rot) | (h << (32 - rot))) >>> 0;
19+
return BASE_PORT + (h & 0xFFF);
20+
}
621

722
/** Filter out already-indexed items by comparing composite keys */
823
export function filterUnindexed(allContent, existingIndex) {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "vector-memory-mcp",
3-
"version": "1.1.0",
3+
"version": "1.2.0",
44
"description": "Semantic vector search MCP server for GitHub Copilot CLI session history",
55
"main": "index.js",
66
"type": "module",

vector-memory-server.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { fileURLToPath } from "url";
88
import { createServer, request as httpReq } from "http";
99
import { execSync } from "child_process";
1010
import { userInfo } from "os";
11-
import { filterUnindexed, dedup, postProcessResults, isOurServer, isIndexable, DIMS, createHandler } from "./lib.js";
11+
import { filterUnindexed, dedup, postProcessResults, isOurServer, isIndexable, DIMS, createHandler, userPort } from "./lib.js";
1212

1313
const __dirname = dirname(fileURLToPath(import.meta.url));
1414
const PKG = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf-8"));
@@ -171,7 +171,7 @@ async function search(vecDb, query, limit = 10) {
171171

172172
// --- HTTP Server (singleton, port 31337) ---
173173

174-
const PORT = parseInt(process.env.VECTOR_MEMORY_PORT || "31337", 10);
174+
const PORT = parseInt(process.env.VECTOR_MEMORY_PORT || String(userPort(SERVER_USER)), 10);
175175

176176
const handleRequest = createHandler({
177177
openVectorDb,

0 commit comments

Comments
 (0)