@openlap/openlap 1.0.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/dist/auth.d.ts +17 -0
- package/dist/auth.js +109 -0
- package/dist/feed.d.ts +23 -0
- package/dist/feed.js +110 -0
- package/dist/git.d.ts +9 -0
- package/dist/git.js +56 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +80 -0
- package/dist/proxy.d.ts +1 -0
- package/dist/proxy.js +114 -0
- package/package.json +40 -0
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
2
|
+
import type { OAuthClientMetadata, OAuthClientInformationFull, OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js";
|
|
3
|
+
export declare class FileOAuthProvider implements OAuthClientProvider {
|
|
4
|
+
authCode: string | null;
|
|
5
|
+
get redirectUrl(): URL;
|
|
6
|
+
get clientMetadata(): OAuthClientMetadata;
|
|
7
|
+
clientInformation(): Promise<OAuthClientInformationFull | undefined>;
|
|
8
|
+
saveClientInformation(info: OAuthClientInformationFull): Promise<void>;
|
|
9
|
+
tokens(): Promise<OAuthTokens | undefined>;
|
|
10
|
+
saveTokens(tokens: OAuthTokens): Promise<void>;
|
|
11
|
+
redirectToAuthorization(authorizationUrl: URL): Promise<void>;
|
|
12
|
+
saveCodeVerifier(codeVerifier: string): Promise<void>;
|
|
13
|
+
codeVerifier(): Promise<string>;
|
|
14
|
+
private waitForCallback;
|
|
15
|
+
}
|
|
16
|
+
export declare function hasToken(): boolean;
|
|
17
|
+
export declare function clearAuth(): void;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { createServer } from "http";
|
|
5
|
+
const CONFIG_DIR = join(homedir(), ".openlap");
|
|
6
|
+
const AUTH_FILE = join(CONFIG_DIR, "auth.json");
|
|
7
|
+
function load() {
|
|
8
|
+
if (!existsSync(AUTH_FILE))
|
|
9
|
+
return {};
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(readFileSync(AUTH_FILE, "utf-8"));
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function save(data) {
|
|
18
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
19
|
+
writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2));
|
|
20
|
+
}
|
|
21
|
+
const CALLBACK_PORT = 19284;
|
|
22
|
+
export class FileOAuthProvider {
|
|
23
|
+
// Captured auth code from callback, used by proxy to call finishAuth
|
|
24
|
+
authCode = null;
|
|
25
|
+
get redirectUrl() {
|
|
26
|
+
return new URL(`http://127.0.0.1:${CALLBACK_PORT}/callback`);
|
|
27
|
+
}
|
|
28
|
+
get clientMetadata() {
|
|
29
|
+
return {
|
|
30
|
+
client_name: "openlap",
|
|
31
|
+
redirect_uris: [`http://127.0.0.1:${CALLBACK_PORT}/callback`],
|
|
32
|
+
grant_types: ["authorization_code"],
|
|
33
|
+
response_types: ["code"],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
async clientInformation() {
|
|
37
|
+
return load().client;
|
|
38
|
+
}
|
|
39
|
+
async saveClientInformation(info) {
|
|
40
|
+
const data = load();
|
|
41
|
+
data.client = info;
|
|
42
|
+
save(data);
|
|
43
|
+
}
|
|
44
|
+
async tokens() {
|
|
45
|
+
return load().tokens;
|
|
46
|
+
}
|
|
47
|
+
async saveTokens(tokens) {
|
|
48
|
+
const data = load();
|
|
49
|
+
data.tokens = tokens;
|
|
50
|
+
save(data);
|
|
51
|
+
}
|
|
52
|
+
async redirectToAuthorization(authorizationUrl) {
|
|
53
|
+
process.stderr.write(`\nOpening browser for authentication...\n`);
|
|
54
|
+
process.stderr.write(`If the browser doesn't open, visit:\n${authorizationUrl.toString()}\n\n`);
|
|
55
|
+
const { default: openUrl } = await import("open");
|
|
56
|
+
await openUrl(authorizationUrl.toString());
|
|
57
|
+
// Wait for callback and capture auth code
|
|
58
|
+
this.authCode = await this.waitForCallback();
|
|
59
|
+
}
|
|
60
|
+
async saveCodeVerifier(codeVerifier) {
|
|
61
|
+
const data = load();
|
|
62
|
+
data.codeVerifier = codeVerifier;
|
|
63
|
+
save(data);
|
|
64
|
+
}
|
|
65
|
+
async codeVerifier() {
|
|
66
|
+
return load().codeVerifier ?? "";
|
|
67
|
+
}
|
|
68
|
+
waitForCallback() {
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
const timeout = setTimeout(() => {
|
|
71
|
+
server.close();
|
|
72
|
+
reject(new Error("OAuth callback timed out after 2 minutes"));
|
|
73
|
+
}, 120_000);
|
|
74
|
+
const server = createServer((req, res) => {
|
|
75
|
+
if (!req.url?.startsWith("/callback")) {
|
|
76
|
+
res.writeHead(404);
|
|
77
|
+
res.end();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const url = new URL(req.url, `http://127.0.0.1:${CALLBACK_PORT}`);
|
|
81
|
+
const code = url.searchParams.get("code");
|
|
82
|
+
if (!code) {
|
|
83
|
+
const error = url.searchParams.get("error") ?? "no code received";
|
|
84
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
85
|
+
res.end(`<html><body><h2>Authentication failed: ${error}</h2></body></html>`);
|
|
86
|
+
clearTimeout(timeout);
|
|
87
|
+
server.close();
|
|
88
|
+
reject(new Error(`OAuth failed: ${error}`));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
92
|
+
res.end("<html><body><h2>Authenticated. You can close this tab.</h2></body></html>");
|
|
93
|
+
clearTimeout(timeout);
|
|
94
|
+
server.close();
|
|
95
|
+
resolve(code);
|
|
96
|
+
});
|
|
97
|
+
server.listen(CALLBACK_PORT, "127.0.0.1");
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export function hasToken() {
|
|
102
|
+
const data = load();
|
|
103
|
+
return !!data.tokens?.access_token;
|
|
104
|
+
}
|
|
105
|
+
export function clearAuth() {
|
|
106
|
+
if (existsSync(AUTH_FILE)) {
|
|
107
|
+
writeFileSync(AUTH_FILE, "{}");
|
|
108
|
+
}
|
|
109
|
+
}
|
package/dist/feed.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
interface TrackUpdate {
|
|
2
|
+
id: string;
|
|
3
|
+
project_name: string;
|
|
4
|
+
body: string;
|
|
5
|
+
health: string;
|
|
6
|
+
created_at: string;
|
|
7
|
+
}
|
|
8
|
+
type UpdateCallback = (tag: string, update: TrackUpdate) => void;
|
|
9
|
+
export declare class FeedManager {
|
|
10
|
+
private baseUrl;
|
|
11
|
+
private streams;
|
|
12
|
+
private seenIds;
|
|
13
|
+
private callback;
|
|
14
|
+
constructor(baseUrl: string, callback: UpdateCallback);
|
|
15
|
+
/**
|
|
16
|
+
* Subscribe to a tag's feed if not already subscribed.
|
|
17
|
+
*/
|
|
18
|
+
subscribe(tag: string): void;
|
|
19
|
+
private connect;
|
|
20
|
+
private readStream;
|
|
21
|
+
stop(): void;
|
|
22
|
+
}
|
|
23
|
+
export {};
|
package/dist/feed.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Experimental: dynamic SSE feed subscription.
|
|
2
|
+
// Auto-subscribes when agent uses get_track or post_update with a tag.
|
|
3
|
+
export class FeedManager {
|
|
4
|
+
baseUrl;
|
|
5
|
+
streams = new Map();
|
|
6
|
+
seenIds = new Set();
|
|
7
|
+
callback;
|
|
8
|
+
constructor(baseUrl, callback) {
|
|
9
|
+
this.baseUrl = baseUrl;
|
|
10
|
+
this.callback = callback;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Subscribe to a tag's feed if not already subscribed.
|
|
14
|
+
*/
|
|
15
|
+
subscribe(tag) {
|
|
16
|
+
if (this.streams.has(tag))
|
|
17
|
+
return;
|
|
18
|
+
process.stderr.write(`[openlap] subscribed to feed: ${tag} (experimental)\n`);
|
|
19
|
+
const controller = new AbortController();
|
|
20
|
+
this.streams.set(tag, controller);
|
|
21
|
+
this.connect(tag, controller);
|
|
22
|
+
}
|
|
23
|
+
connect(tag, controller) {
|
|
24
|
+
const url = `${this.baseUrl}/feed/${tag}/sse`;
|
|
25
|
+
fetch(url, { signal: controller.signal })
|
|
26
|
+
.then((res) => {
|
|
27
|
+
if (!res.ok || !res.body) {
|
|
28
|
+
throw new Error(`SSE connect failed: ${res.status}`);
|
|
29
|
+
}
|
|
30
|
+
return this.readStream(tag, res.body, controller);
|
|
31
|
+
})
|
|
32
|
+
.catch((err) => {
|
|
33
|
+
if (err.name === "AbortError")
|
|
34
|
+
return;
|
|
35
|
+
// Reconnect after 5s
|
|
36
|
+
if (this.streams.has(tag)) {
|
|
37
|
+
setTimeout(() => this.connect(tag, controller), 5000);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
async readStream(tag, body, controller) {
|
|
42
|
+
const reader = body.getReader();
|
|
43
|
+
const decoder = new TextDecoder();
|
|
44
|
+
let buffer = "";
|
|
45
|
+
let eventType = "";
|
|
46
|
+
let eventData = "";
|
|
47
|
+
let eventId = "";
|
|
48
|
+
try {
|
|
49
|
+
while (true) {
|
|
50
|
+
const { done, value } = await reader.read();
|
|
51
|
+
if (done)
|
|
52
|
+
break;
|
|
53
|
+
buffer += decoder.decode(value, { stream: true });
|
|
54
|
+
const lines = buffer.split("\n");
|
|
55
|
+
buffer = lines.pop() ?? "";
|
|
56
|
+
for (const line of lines) {
|
|
57
|
+
if (line.startsWith("event: ")) {
|
|
58
|
+
eventType = line.slice(7);
|
|
59
|
+
}
|
|
60
|
+
else if (line.startsWith("data: ")) {
|
|
61
|
+
eventData = line.slice(6);
|
|
62
|
+
}
|
|
63
|
+
else if (line.startsWith("id: ")) {
|
|
64
|
+
eventId = line.slice(4);
|
|
65
|
+
}
|
|
66
|
+
else if (line === "") {
|
|
67
|
+
if (eventType === "update" && eventData && eventId) {
|
|
68
|
+
if (!this.seenIds.has(eventId)) {
|
|
69
|
+
this.seenIds.add(eventId);
|
|
70
|
+
// Cap at 500
|
|
71
|
+
if (this.seenIds.size > 500) {
|
|
72
|
+
const iter = this.seenIds.values();
|
|
73
|
+
for (let i = 0; i < 100; i++)
|
|
74
|
+
iter.next();
|
|
75
|
+
const toKeep = new Set();
|
|
76
|
+
for (const v of iter)
|
|
77
|
+
toKeep.add(v);
|
|
78
|
+
this.seenIds = toKeep;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const update = JSON.parse(eventData);
|
|
82
|
+
this.callback(tag, update);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// skip malformed
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
eventType = "";
|
|
90
|
+
eventData = "";
|
|
91
|
+
eventId = "";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// stream ended
|
|
98
|
+
}
|
|
99
|
+
// Reconnect if still subscribed
|
|
100
|
+
if (this.streams.has(tag) && !controller.signal.aborted) {
|
|
101
|
+
setTimeout(() => this.connect(tag, controller), 5000);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
stop() {
|
|
105
|
+
for (const [, controller] of this.streams) {
|
|
106
|
+
controller.abort();
|
|
107
|
+
}
|
|
108
|
+
this.streams.clear();
|
|
109
|
+
}
|
|
110
|
+
}
|
package/dist/git.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect project (owner/repo) from git remote origin.
|
|
3
|
+
*/
|
|
4
|
+
export declare function detectProject(): string | null;
|
|
5
|
+
/**
|
|
6
|
+
* Auto-save: git add, commit, push before post_update.
|
|
7
|
+
* Returns the commit message used, or null if nothing to commit.
|
|
8
|
+
*/
|
|
9
|
+
export declare function autoSave(body: string): string | null;
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
/**
|
|
3
|
+
* Detect project (owner/repo) from git remote origin.
|
|
4
|
+
*/
|
|
5
|
+
export function detectProject() {
|
|
6
|
+
try {
|
|
7
|
+
const remote = execSync("git remote get-url origin", {
|
|
8
|
+
encoding: "utf-8",
|
|
9
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
10
|
+
}).trim();
|
|
11
|
+
// Handle SSH: git@github.com:owner/repo.git
|
|
12
|
+
const sshMatch = remote.match(/git@[^:]+:(.+?)(?:\.git)?$/);
|
|
13
|
+
if (sshMatch)
|
|
14
|
+
return sshMatch[1];
|
|
15
|
+
// Handle HTTPS: https://github.com/owner/repo.git
|
|
16
|
+
const httpsMatch = remote.match(/https?:\/\/[^/]+\/(.+?)(?:\.git)?$/);
|
|
17
|
+
if (httpsMatch)
|
|
18
|
+
return httpsMatch[1];
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Auto-save: git add, commit, push before post_update.
|
|
27
|
+
* Returns the commit message used, or null if nothing to commit.
|
|
28
|
+
*/
|
|
29
|
+
export function autoSave(body) {
|
|
30
|
+
try {
|
|
31
|
+
// Stage all changes
|
|
32
|
+
execSync("git add -A", { stdio: "pipe" });
|
|
33
|
+
// Check if there are staged changes
|
|
34
|
+
try {
|
|
35
|
+
execSync("git diff --cached --quiet", { stdio: "pipe" });
|
|
36
|
+
return null; // nothing staged
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// diff --cached --quiet exits 1 when there are changes
|
|
40
|
+
}
|
|
41
|
+
// Commit with first line of update body as message
|
|
42
|
+
const msg = body.split("\n")[0].slice(0, 72);
|
|
43
|
+
execSync(`git commit -m ${JSON.stringify(msg)}`, { stdio: "pipe" });
|
|
44
|
+
// Push (best effort)
|
|
45
|
+
try {
|
|
46
|
+
execSync("git push", { stdio: "pipe" });
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// push failure shouldn't block the update
|
|
50
|
+
}
|
|
51
|
+
return msg;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
const command = process.argv[2];
|
|
4
|
+
// -- Setup subcommand: adds MCP server to Claude Code -----------------------
|
|
5
|
+
if (command === "setup") {
|
|
6
|
+
console.log("Setting up openlap...");
|
|
7
|
+
console.log("");
|
|
8
|
+
try {
|
|
9
|
+
// Add new "openlap" entry first (safe -- if this fails, old config still works)
|
|
10
|
+
let added = false;
|
|
11
|
+
try {
|
|
12
|
+
execSync(`claude mcp add -s user openlap -- npx -y @openlap/openlap`, { stdio: "pipe" });
|
|
13
|
+
added = true;
|
|
14
|
+
console.log("Added openlap MCP server.");
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
const msg = err instanceof Error ? err.stderr?.toString() ?? err.message : String(err);
|
|
18
|
+
if (msg.includes("already exists")) {
|
|
19
|
+
console.log("openlap MCP server already installed.");
|
|
20
|
+
added = true;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
throw err;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// Only remove old entries after new one is confirmed
|
|
27
|
+
if (added) {
|
|
28
|
+
// Remove old "anylap" entry if present (migration from @openlap/anylap)
|
|
29
|
+
try {
|
|
30
|
+
execSync("claude mcp remove anylap", { stdio: "pipe" });
|
|
31
|
+
console.log("Migrated: removed old 'anylap' entry.");
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Not present, fine
|
|
35
|
+
}
|
|
36
|
+
// Remove old "lap" HTTP entry if present (migration from direct HTTP setup)
|
|
37
|
+
try {
|
|
38
|
+
execSync("claude mcp remove lap", { stdio: "pipe" });
|
|
39
|
+
console.log("Migrated: removed old 'lap' HTTP entry.");
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Not present, fine
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
console.log("");
|
|
46
|
+
console.log("Done. Open Claude Code -- first message triggers GitHub login.");
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
console.error("Failed to add MCP server. Is Claude Code installed?");
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
// -- Login subcommand: force re-authentication ------------------------------
|
|
55
|
+
if (command === "login") {
|
|
56
|
+
const { clearAuth } = await import("./auth.js");
|
|
57
|
+
clearAuth();
|
|
58
|
+
console.log("Cleared stored credentials. Next Claude Code session will re-authenticate.");
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
61
|
+
// -- Help -------------------------------------------------------------------
|
|
62
|
+
if (command === "help" || command === "--help" || command === "-h") {
|
|
63
|
+
console.log("Usage: npx @openlap/openlap <command>");
|
|
64
|
+
console.log("");
|
|
65
|
+
console.log("Commands:");
|
|
66
|
+
console.log(" setup Add openlap MCP server to Claude Code");
|
|
67
|
+
console.log(" login Clear stored credentials and re-authenticate");
|
|
68
|
+
console.log(" help Show this help");
|
|
69
|
+
console.log("");
|
|
70
|
+
console.log("When run without arguments, starts the MCP proxy server (used by Claude Code).");
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
// -- Default: run as MCP proxy server (called by Claude Code) ---------------
|
|
74
|
+
if (command && command !== "serve") {
|
|
75
|
+
console.error(`Unknown command: ${command}`);
|
|
76
|
+
console.error("Run 'npx @openlap/openlap help' for usage.");
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
const { startProxy } = await import("./proxy.js");
|
|
80
|
+
await startProxy();
|
package/dist/proxy.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function startProxy(): Promise<void>;
|
package/dist/proxy.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
4
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
5
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
+
import { FileOAuthProvider } from "./auth.js";
|
|
7
|
+
import { detectProject, autoSave } from "./git.js";
|
|
8
|
+
import { FeedManager } from "./feed.js";
|
|
9
|
+
const BASE_URL = process.env.ANYLAP_URL ?? "https://openlap.app";
|
|
10
|
+
// Tools that accept a 'project' parameter
|
|
11
|
+
const PROJECT_TOOLS = new Set([
|
|
12
|
+
"list_laps", "get_lap", "create_lap", "save_lap", "update_lap",
|
|
13
|
+
"post_update", "list_updates",
|
|
14
|
+
"create_project", "update_project", "remove_project",
|
|
15
|
+
]);
|
|
16
|
+
export async function startProxy() {
|
|
17
|
+
const authProvider = new FileOAuthProvider();
|
|
18
|
+
// -- Connect to remote MCP server ----------------------------------------
|
|
19
|
+
const remoteTransport = new StreamableHTTPClientTransport(new URL(`${BASE_URL}/mcp`), { authProvider });
|
|
20
|
+
const remote = new Client({ name: "openlap-proxy", version: "1.0.0" }, { capabilities: {} });
|
|
21
|
+
// Try connecting -- if auth is needed, the provider opens the browser
|
|
22
|
+
try {
|
|
23
|
+
await remote.connect(remoteTransport);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
// After redirectToAuthorization, the SDK throws UnauthorizedError.
|
|
27
|
+
// We have the auth code from the callback -- finish the flow.
|
|
28
|
+
if (authProvider.authCode) {
|
|
29
|
+
await remoteTransport.finishAuth(authProvider.authCode);
|
|
30
|
+
// Retry connection
|
|
31
|
+
await remote.connect(remoteTransport);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
35
|
+
process.stderr.write(`[openlap] auth error: ${msg}\n`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// -- Auto-detect project from git remote ----------------------------------
|
|
40
|
+
const detectedProject = detectProject();
|
|
41
|
+
if (detectedProject) {
|
|
42
|
+
process.stderr.write(`[openlap] project: ${detectedProject}\n`);
|
|
43
|
+
}
|
|
44
|
+
// -- Local MCP server (stdio) ---------------------------------------------
|
|
45
|
+
const server = new Server({ name: "openlap", version: "1.0.0" }, {
|
|
46
|
+
capabilities: {
|
|
47
|
+
tools: {},
|
|
48
|
+
experimental: { "claude/channel": {} },
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
// -- Experimental: dynamic feed subscription ------------------------------
|
|
52
|
+
const feeds = new FeedManager(BASE_URL, (tag, update) => {
|
|
53
|
+
const who = update.project_name || "unknown";
|
|
54
|
+
const health = update.health && update.health !== "on_track" ? ` [${update.health}]` : "";
|
|
55
|
+
const content = `[${who}]${health} ${update.body}`;
|
|
56
|
+
process.stderr.write(`[openlap] feed ${tag}: ${content}\n`);
|
|
57
|
+
server.notification({
|
|
58
|
+
method: "notifications/claude/channel",
|
|
59
|
+
params: {
|
|
60
|
+
content,
|
|
61
|
+
meta: {
|
|
62
|
+
update_id: update.id,
|
|
63
|
+
tag,
|
|
64
|
+
created_at: update.created_at,
|
|
65
|
+
health: update.health ?? "on_track",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
}).catch((err) => {
|
|
69
|
+
if (!String(err).includes("Not connected")) {
|
|
70
|
+
process.stderr.write(`[openlap] notification error: ${err}\n`);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
// Forward tool listing from remote
|
|
75
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
76
|
+
const result = await remote.listTools();
|
|
77
|
+
return { tools: result.tools };
|
|
78
|
+
});
|
|
79
|
+
// Forward tool calls with local enhancements
|
|
80
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
81
|
+
const { name } = req.params;
|
|
82
|
+
const args = { ...(req.params.arguments ?? {}) };
|
|
83
|
+
// Auto-inject project if not provided and detectable
|
|
84
|
+
if (detectedProject && PROJECT_TOOLS.has(name) && !args.project) {
|
|
85
|
+
args.project = detectedProject;
|
|
86
|
+
}
|
|
87
|
+
// Auto-save before post_update
|
|
88
|
+
if (name === "post_update" && typeof args.body === "string") {
|
|
89
|
+
const saved = autoSave(args.body);
|
|
90
|
+
if (saved) {
|
|
91
|
+
process.stderr.write(`[openlap] auto-saved: ${saved}\n`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Forward to remote
|
|
95
|
+
const result = await remote.callTool({ name, arguments: args });
|
|
96
|
+
// Experimental: auto-subscribe to feeds from tool usage
|
|
97
|
+
if (typeof args.tag === "string" && args.tag) {
|
|
98
|
+
feeds.subscribe(args.tag);
|
|
99
|
+
}
|
|
100
|
+
if (name === "get_track" && typeof args.name === "string" && args.name) {
|
|
101
|
+
feeds.subscribe(args.name);
|
|
102
|
+
}
|
|
103
|
+
if (name === "create_track" && typeof args.name === "string" && args.name) {
|
|
104
|
+
feeds.subscribe(args.name);
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
content: result.content,
|
|
108
|
+
isError: result.isError,
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
// -- Start stdio transport ------------------------------------------------
|
|
112
|
+
const transport = new StdioServerTransport();
|
|
113
|
+
await server.connect(transport);
|
|
114
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@openlap/openlap",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Local MCP proxy for openlap.app -- auto-save, live feeds, project detection, one install",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"openlap": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"prepublishOnly": "npm run build",
|
|
16
|
+
"dev": "npx tsx src/index.ts"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
20
|
+
"open": "^10.0.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^25.5.0",
|
|
24
|
+
"typescript": "^5.0.0"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18"
|
|
28
|
+
},
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/wildreason/openlap"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"claude-code",
|
|
36
|
+
"mcp",
|
|
37
|
+
"openlap",
|
|
38
|
+
"agent-coordination"
|
|
39
|
+
]
|
|
40
|
+
}
|