@kitae9999/openlog-cli 1.0.0 → 1.1.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 +20 -11
- package/dist/api-client.js +15 -1
- package/dist/config.js +4 -0
- package/dist/index.js +12 -32
- package/dist/mcp-install.js +5 -1
- package/dist/mcp-permissions-command.js +45 -0
- package/dist/mcp-permissions.js +103 -0
- package/dist/mcp-server.js +89 -93
- package/dist/mcp-toolkit.js +115 -0
- package/dist/mcp-workspace-tools.js +645 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,17 +8,24 @@ npx -y @kitae9999/openlog-cli whoami
|
|
|
8
8
|
npx -y @kitae9999/openlog-cli mcp
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
MCP
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
-
|
|
11
|
+
Local MCP permissions:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
openlog mcp permissions
|
|
15
|
+
openlog mcp permissions set read-only
|
|
16
|
+
openlog mcp permissions set safe-write
|
|
17
|
+
openlog mcp permissions set full
|
|
18
|
+
openlog mcp permissions reset
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`safe-write` is the default. Permission changes apply after the MCP server is restarted or reloaded.
|
|
22
|
+
|
|
23
|
+
MCP tool groups:
|
|
24
|
+
|
|
25
|
+
- Account and posts: authentication, notifications, authored/liked posts, image upload, post detail, and confirmed publishing
|
|
26
|
+
- Workspace context: workspace discovery, activity, working brief, tasks, logs, todos, memories, outputs, and graph links
|
|
27
|
+
- Safe writes: create/update, todo completion, link creation, image upload, and confirmed output/post publishing
|
|
28
|
+
- Full-only writes: individual workspace-document/link deletes and working-brief clear
|
|
22
29
|
|
|
23
30
|
MCP client configuration:
|
|
24
31
|
|
|
@@ -35,3 +42,5 @@ MCP client configuration:
|
|
|
35
42
|
|
|
36
43
|
Set `OPENLOG_API_BASE_URL` to point at a non-production API. Set
|
|
37
44
|
`OPENLOG_WEB_BASE_URL` to control absolute post URLs returned by `publish_post`.
|
|
45
|
+
Set `OPENLOG_MCP_CONFIG_FILE` to replace the default
|
|
46
|
+
`~/.openlog/mcp-config.json` permission file.
|
package/dist/api-client.js
CHANGED
|
@@ -38,6 +38,13 @@ export class OpenLogApiClient {
|
|
|
38
38
|
body: body ? JSON.stringify(body) : undefined,
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
|
+
async patch(path, body) {
|
|
42
|
+
return this.request(path, {
|
|
43
|
+
method: "PATCH",
|
|
44
|
+
headers: body ? { "content-type": "application/json" } : undefined,
|
|
45
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
41
48
|
async postNoContent(path, body) {
|
|
42
49
|
await this.request(path, {
|
|
43
50
|
method: "POST",
|
|
@@ -52,6 +59,9 @@ export class OpenLogApiClient {
|
|
|
52
59
|
body: body ? JSON.stringify(body) : undefined,
|
|
53
60
|
});
|
|
54
61
|
}
|
|
62
|
+
async deleteNoContent(path) {
|
|
63
|
+
await this.request(path, { method: "DELETE" });
|
|
64
|
+
}
|
|
55
65
|
async request(path, init, allowRefresh = true) {
|
|
56
66
|
const url = `${this.apiBaseUrl}${path}`;
|
|
57
67
|
const response = await fetch(url, {
|
|
@@ -73,7 +83,11 @@ export class OpenLogApiClient {
|
|
|
73
83
|
if (response.status === 204) {
|
|
74
84
|
return undefined;
|
|
75
85
|
}
|
|
76
|
-
|
|
86
|
+
const responseBody = await response.text();
|
|
87
|
+
if (!responseBody) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
return JSON.parse(responseBody);
|
|
77
91
|
}
|
|
78
92
|
async refreshAccessToken() {
|
|
79
93
|
if (!this.refreshPromise) {
|
package/dist/config.js
CHANGED
|
@@ -12,3 +12,7 @@ export function getAuthFilePath() {
|
|
|
12
12
|
return (process.env.OPENLOG_AUTH_FILE ??
|
|
13
13
|
path.join(os.homedir(), ".openlog", "auth.json"));
|
|
14
14
|
}
|
|
15
|
+
export function getMcpConfigFilePath() {
|
|
16
|
+
return (process.env.OPENLOG_MCP_CONFIG_FILE ??
|
|
17
|
+
path.join(os.homedir(), ".openlog", "mcp-config.json"));
|
|
18
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { runMcpServer } from "./mcp-server.js";
|
|
|
5
5
|
import { installMcp, printMcpInstallHelp } from "./mcp-install.js";
|
|
6
6
|
import { ApiError, OpenLogApiClient } from "./api-client.js";
|
|
7
7
|
import { createAuthenticatedApiClient } from "./authenticated-client.js";
|
|
8
|
+
import { runMcpPermissionsCommand } from "./mcp-permissions-command.js";
|
|
8
9
|
const command = process.argv[2] ?? "help";
|
|
9
10
|
const subcommand = process.argv[3];
|
|
10
11
|
try {
|
|
@@ -21,10 +22,12 @@ try {
|
|
|
21
22
|
if (subcommand === "install") {
|
|
22
23
|
await installMcpCommand();
|
|
23
24
|
}
|
|
25
|
+
else if (subcommand === "permissions") {
|
|
26
|
+
await runMcpPermissionsCommand(process.argv.slice(4));
|
|
27
|
+
}
|
|
24
28
|
else if (subcommand === undefined ||
|
|
25
29
|
subcommand === "serve" ||
|
|
26
30
|
subcommand === "start") {
|
|
27
|
-
printMcpStartupHint();
|
|
28
31
|
await runMcpServer();
|
|
29
32
|
}
|
|
30
33
|
else {
|
|
@@ -78,37 +81,6 @@ async function logout() {
|
|
|
78
81
|
await deleteAuthFile();
|
|
79
82
|
console.log("Logged out.");
|
|
80
83
|
}
|
|
81
|
-
function printMcpStartupHint() {
|
|
82
|
-
if (!process.stderr.isTTY) {
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
console.error(`OpenLog MCP server is running over stdio.
|
|
86
|
-
|
|
87
|
-
This terminal is now reserved for MCP protocol traffic.
|
|
88
|
-
Press Ctrl+C to stop it.
|
|
89
|
-
|
|
90
|
-
To use it from an MCP client, add:
|
|
91
|
-
{
|
|
92
|
-
"mcpServers": {
|
|
93
|
-
"openlog": {
|
|
94
|
-
"command": "openlog",
|
|
95
|
-
"args": ["mcp"]
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
Available tools:
|
|
101
|
-
get_auth_status
|
|
102
|
-
get_me
|
|
103
|
-
list_my_notifications
|
|
104
|
-
list_my_posts
|
|
105
|
-
list_my_liked_posts
|
|
106
|
-
upload_post_image
|
|
107
|
-
publish_post
|
|
108
|
-
push_working_brief
|
|
109
|
-
get_post_detail
|
|
110
|
-
`);
|
|
111
|
-
}
|
|
112
84
|
function printHelp() {
|
|
113
85
|
console.log(`OpenLog CLI
|
|
114
86
|
|
|
@@ -121,11 +93,19 @@ Usage:
|
|
|
121
93
|
Register OpenLog MCP with Codex
|
|
122
94
|
openlog mcp install claude-code
|
|
123
95
|
Register OpenLog MCP with Claude Code
|
|
96
|
+
openlog mcp permissions
|
|
97
|
+
Show the active local MCP permission profile
|
|
98
|
+
openlog mcp permissions set read-only|safe-write|full
|
|
99
|
+
Change the local MCP permission profile
|
|
100
|
+
openlog mcp permissions reset
|
|
101
|
+
Restore the default safe-write profile
|
|
124
102
|
|
|
125
103
|
Environment:
|
|
126
104
|
OPENLOG_API_BASE_URL Override the OpenLog API base URL
|
|
127
105
|
OPENLOG_WEB_BASE_URL Override the OpenLog web base URL for published post links
|
|
128
106
|
OPENLOG_AUTH_FILE Override the local auth file path
|
|
107
|
+
OPENLOG_MCP_CONFIG_FILE
|
|
108
|
+
Override the local MCP permission config path
|
|
129
109
|
`);
|
|
130
110
|
}
|
|
131
111
|
function formatCliError(error) {
|
package/dist/mcp-install.js
CHANGED
|
@@ -28,7 +28,8 @@ Aliases:
|
|
|
28
28
|
claude Same as claude-code
|
|
29
29
|
|
|
30
30
|
Notes:
|
|
31
|
-
If OPENLOG_API_BASE_URL or
|
|
31
|
+
If OPENLOG_API_BASE_URL, OPENLOG_WEB_BASE_URL, or OPENLOG_MCP_CONFIG_FILE is set,
|
|
32
|
+
it is added to the client config.
|
|
32
33
|
`);
|
|
33
34
|
}
|
|
34
35
|
function buildServerConfig() {
|
|
@@ -41,6 +42,9 @@ function buildServerConfig() {
|
|
|
41
42
|
if (webBaseUrl !== DEFAULT_WEB_BASE_URL) {
|
|
42
43
|
env.OPENLOG_WEB_BASE_URL = webBaseUrl;
|
|
43
44
|
}
|
|
45
|
+
if (process.env.OPENLOG_MCP_CONFIG_FILE) {
|
|
46
|
+
env.OPENLOG_MCP_CONFIG_FILE = process.env.OPENLOG_MCP_CONFIG_FILE;
|
|
47
|
+
}
|
|
44
48
|
return {
|
|
45
49
|
command: "openlog",
|
|
46
50
|
args: ["mcp"],
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { isMcpPermissionProfile, readMcpPermissions, resetMcpPermissions, writeMcpPermissionProfile, } from "./mcp-permissions.js";
|
|
2
|
+
export async function runMcpPermissionsCommand(args, writeOutput = console.log) {
|
|
3
|
+
const action = args[0] ?? "show";
|
|
4
|
+
if ((action === "show" && args.length === 1) || args.length === 0) {
|
|
5
|
+
writeOutput(formatPermissions(await readMcpPermissions()));
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
if (action === "set" && args.length === 2) {
|
|
9
|
+
const profile = args[1];
|
|
10
|
+
if (!isMcpPermissionProfile(profile)) {
|
|
11
|
+
throw new Error(permissionUsage());
|
|
12
|
+
}
|
|
13
|
+
const permissions = await writeMcpPermissionProfile(profile);
|
|
14
|
+
writeOutput(formatPermissions(permissions));
|
|
15
|
+
// 실행 중인 MCP server는 시작 시 권한을 확정하므로 재시작 안내가 필요하다.
|
|
16
|
+
writeOutput("Restart or reload the OpenLog MCP server to apply this profile.");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (action === "reset" && args.length === 1) {
|
|
20
|
+
const permissions = await resetMcpPermissions();
|
|
21
|
+
writeOutput(formatPermissions(permissions));
|
|
22
|
+
writeOutput("Restart or reload the OpenLog MCP server to apply this profile.");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
throw new Error(permissionUsage());
|
|
26
|
+
}
|
|
27
|
+
export function permissionUsage() {
|
|
28
|
+
return `OpenLog MCP permissions
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
openlog mcp permissions
|
|
32
|
+
openlog mcp permissions show
|
|
33
|
+
openlog mcp permissions set read-only
|
|
34
|
+
openlog mcp permissions set safe-write
|
|
35
|
+
openlog mcp permissions set full
|
|
36
|
+
openlog mcp permissions reset`;
|
|
37
|
+
}
|
|
38
|
+
function formatPermissions(permissions) {
|
|
39
|
+
return [
|
|
40
|
+
`Profile: ${permissions.profile}`,
|
|
41
|
+
`Capabilities: ${permissions.capabilities.join(", ")}`,
|
|
42
|
+
`Source: ${permissions.configured ? "local config" : "default"}`,
|
|
43
|
+
...(permissions.updatedAt ? [`Updated: ${permissions.updatedAt}`] : []),
|
|
44
|
+
].join("\n");
|
|
45
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getMcpConfigFilePath } from "./config.js";
|
|
4
|
+
export const MCP_PERMISSION_PROFILES = [
|
|
5
|
+
"read-only",
|
|
6
|
+
"safe-write",
|
|
7
|
+
"full",
|
|
8
|
+
];
|
|
9
|
+
export const MCP_CAPABILITIES = ["read", "write", "publish", "delete"];
|
|
10
|
+
export const DEFAULT_MCP_PERMISSION_PROFILE = "safe-write";
|
|
11
|
+
const PROFILE_CAPABILITIES = {
|
|
12
|
+
// 프로필은 상위 프로필이 하위 capability를 모두 포함하도록 누적 구성한다.
|
|
13
|
+
"read-only": ["read"],
|
|
14
|
+
"safe-write": ["read", "write", "publish"],
|
|
15
|
+
full: ["read", "write", "publish", "delete"],
|
|
16
|
+
};
|
|
17
|
+
export async function readMcpPermissions() {
|
|
18
|
+
const filePath = getMcpConfigFilePath();
|
|
19
|
+
let content;
|
|
20
|
+
try {
|
|
21
|
+
content = await readFile(filePath, "utf8");
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (hasErrorCode(error, "ENOENT")) {
|
|
25
|
+
// 설정 파일이 없는 최초 실행은 기존 동작과 호환되는 safe-write를 사용한다.
|
|
26
|
+
return resolvePermissions(DEFAULT_MCP_PERMISSION_PROFILE, false, null);
|
|
27
|
+
}
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
let parsed;
|
|
31
|
+
try {
|
|
32
|
+
parsed = JSON.parse(content);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
throw invalidConfigError(filePath, "file is not valid JSON");
|
|
36
|
+
}
|
|
37
|
+
const config = parsePermissionConfig(parsed, filePath);
|
|
38
|
+
return resolvePermissions(config.profile, true, config.updatedAt);
|
|
39
|
+
}
|
|
40
|
+
export async function writeMcpPermissionProfile(profile) {
|
|
41
|
+
const filePath = getMcpConfigFilePath();
|
|
42
|
+
const config = {
|
|
43
|
+
version: 1,
|
|
44
|
+
profile,
|
|
45
|
+
updatedAt: new Date().toISOString(),
|
|
46
|
+
};
|
|
47
|
+
await mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
48
|
+
await writeFile(filePath, `${JSON.stringify(config, null, 2)}\n`, {
|
|
49
|
+
mode: 0o600,
|
|
50
|
+
});
|
|
51
|
+
// 기존 파일을 덮어쓸 때도 소유자만 읽고 쓸 수 있도록 권한을 다시 고정한다.
|
|
52
|
+
await chmod(filePath, 0o600);
|
|
53
|
+
return resolvePermissions(profile, true, config.updatedAt);
|
|
54
|
+
}
|
|
55
|
+
export async function resetMcpPermissions() {
|
|
56
|
+
await rm(getMcpConfigFilePath(), { force: true });
|
|
57
|
+
return resolvePermissions(DEFAULT_MCP_PERMISSION_PROFILE, false, null);
|
|
58
|
+
}
|
|
59
|
+
export function isMcpPermissionProfile(value) {
|
|
60
|
+
return MCP_PERMISSION_PROFILES.some((profile) => profile === value);
|
|
61
|
+
}
|
|
62
|
+
export function hasMcpCapability(permissions, capability) {
|
|
63
|
+
return permissions.capabilities.includes(capability);
|
|
64
|
+
}
|
|
65
|
+
function resolvePermissions(profile, configured, updatedAt) {
|
|
66
|
+
return {
|
|
67
|
+
profile,
|
|
68
|
+
capabilities: [...PROFILE_CAPABILITIES[profile]],
|
|
69
|
+
configured,
|
|
70
|
+
updatedAt,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function parsePermissionConfig(value, filePath) {
|
|
74
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
75
|
+
throw invalidConfigError(filePath, "root value must be an object");
|
|
76
|
+
}
|
|
77
|
+
const config = value;
|
|
78
|
+
if (config.version !== 1) {
|
|
79
|
+
throw invalidConfigError(filePath, "version must be 1");
|
|
80
|
+
}
|
|
81
|
+
if (typeof config.profile !== "string" ||
|
|
82
|
+
!isMcpPermissionProfile(config.profile)) {
|
|
83
|
+
throw invalidConfigError(filePath, `profile must be one of: ${MCP_PERMISSION_PROFILES.join(", ")}`);
|
|
84
|
+
}
|
|
85
|
+
if (typeof config.updatedAt !== "string" ||
|
|
86
|
+
!Number.isFinite(Date.parse(config.updatedAt))) {
|
|
87
|
+
throw invalidConfigError(filePath, "updatedAt must be an ISO date-time");
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
version: 1,
|
|
91
|
+
profile: config.profile,
|
|
92
|
+
updatedAt: config.updatedAt,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function invalidConfigError(filePath, reason) {
|
|
96
|
+
return new Error(`Invalid OpenLog MCP config at ${filePath}: ${reason}. Run \`openlog mcp permissions reset\` to restore the safe-write default.`);
|
|
97
|
+
}
|
|
98
|
+
function hasErrorCode(error, code) {
|
|
99
|
+
return Boolean(error &&
|
|
100
|
+
typeof error === "object" &&
|
|
101
|
+
"code" in error &&
|
|
102
|
+
error.code === code);
|
|
103
|
+
}
|
package/dist/mcp-server.js
CHANGED
|
@@ -4,81 +4,100 @@ import { z } from "zod";
|
|
|
4
4
|
import { readAuthFile } from "./auth-store.js";
|
|
5
5
|
import { createAuthenticatedApiClient } from "./authenticated-client.js";
|
|
6
6
|
import { getApiBaseUrl, getWebBaseUrl } from "./config.js";
|
|
7
|
+
import { readMcpPermissions, } from "./mcp-permissions.js";
|
|
8
|
+
import { createContentPreview, McpToolRegistry, READ_TOOL_ANNOTATIONS, WRITE_TOOL_ANNOTATIONS, } from "./mcp-toolkit.js";
|
|
9
|
+
import { registerWorkspaceTools } from "./mcp-workspace-tools.js";
|
|
7
10
|
import { uploadPostImage } from "./post-image-upload.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
export async function createOpenLogMcpServer(options = {}) {
|
|
12
|
+
// 의존성을 주입할 수 있는 factory로 구성해 실제 네트워크 없이 도구 목록과 호출을 테스트한다.
|
|
13
|
+
const permissions = options.permissions ?? (await readMcpPermissions());
|
|
14
|
+
const createClient = options.createAuthenticatedClient ?? createAuthenticatedApiClient;
|
|
15
|
+
const readAuth = options.readAuth ?? readAuthFile;
|
|
16
|
+
const uploadImage = options.uploadImage ?? uploadPostImage;
|
|
17
|
+
const apiBaseUrl = options.apiBaseUrl ?? getApiBaseUrl();
|
|
18
|
+
const webBaseUrl = options.webBaseUrl ?? getWebBaseUrl();
|
|
15
19
|
const server = new McpServer({
|
|
16
20
|
name: "openlog",
|
|
17
21
|
version: "1.0.0",
|
|
18
22
|
});
|
|
19
|
-
server
|
|
23
|
+
const registry = new McpToolRegistry(server, permissions, createClient);
|
|
24
|
+
registry.registerLocal("get_mcp_permissions", "read", {
|
|
25
|
+
title: "Get OpenLog MCP Permissions",
|
|
26
|
+
description: "Return the active local MCP permission profile and capabilities.",
|
|
27
|
+
inputSchema: {},
|
|
28
|
+
annotations: READ_TOOL_ANNOTATIONS,
|
|
29
|
+
}, async () => ({
|
|
30
|
+
...permissions,
|
|
31
|
+
note: "This is a local agent safety policy, not a server-side authorization boundary.",
|
|
32
|
+
}));
|
|
33
|
+
registry.registerLocal("get_auth_status", "read", {
|
|
20
34
|
title: "Get OpenLog Auth Status",
|
|
21
35
|
description: "Check whether the local OpenLog CLI is authenticated.",
|
|
22
36
|
inputSchema: {},
|
|
37
|
+
annotations: READ_TOOL_ANNOTATIONS,
|
|
23
38
|
}, async () => {
|
|
24
|
-
let
|
|
39
|
+
let resolvedApiBaseUrl = apiBaseUrl;
|
|
25
40
|
try {
|
|
26
|
-
const authFile = await
|
|
41
|
+
const authFile = await readAuth();
|
|
27
42
|
if (!authFile) {
|
|
28
|
-
return
|
|
43
|
+
return {
|
|
29
44
|
authenticated: false,
|
|
30
|
-
apiBaseUrl,
|
|
31
|
-
}
|
|
45
|
+
apiBaseUrl: resolvedApiBaseUrl,
|
|
46
|
+
};
|
|
32
47
|
}
|
|
33
|
-
|
|
34
|
-
const me = await
|
|
35
|
-
return
|
|
48
|
+
resolvedApiBaseUrl = authFile.apiBaseUrl;
|
|
49
|
+
const me = await createClient().then((client) => client.get("/auth/me"));
|
|
50
|
+
return {
|
|
36
51
|
authenticated: true,
|
|
37
52
|
apiBaseUrl: authFile.apiBaseUrl,
|
|
38
53
|
user: me,
|
|
39
|
-
}
|
|
54
|
+
};
|
|
40
55
|
}
|
|
41
56
|
catch (error) {
|
|
42
|
-
return
|
|
57
|
+
return {
|
|
43
58
|
authenticated: false,
|
|
44
|
-
apiBaseUrl,
|
|
59
|
+
apiBaseUrl: resolvedApiBaseUrl,
|
|
45
60
|
error: error instanceof Error ? error.message : String(error),
|
|
46
|
-
}
|
|
61
|
+
};
|
|
47
62
|
}
|
|
48
63
|
});
|
|
49
|
-
|
|
64
|
+
registry.registerAuthenticated("get_me", "read", {
|
|
50
65
|
title: "Get OpenLog Me",
|
|
51
66
|
description: "Return the currently authenticated OpenLog user.",
|
|
52
67
|
inputSchema: {},
|
|
53
|
-
|
|
54
|
-
|
|
68
|
+
annotations: READ_TOOL_ANNOTATIONS,
|
|
69
|
+
}, (client) => client.get("/auth/me"));
|
|
70
|
+
registry.registerAuthenticated("list_my_notifications", "read", {
|
|
55
71
|
title: "List My OpenLog Notifications",
|
|
56
72
|
description: "Return notifications for the authenticated OpenLog user.",
|
|
57
73
|
inputSchema: {
|
|
58
74
|
size: z.number().int().min(1).max(20).default(20),
|
|
59
75
|
},
|
|
60
|
-
|
|
61
|
-
|
|
76
|
+
annotations: READ_TOOL_ANNOTATIONS,
|
|
77
|
+
}, (client, { size }) => client.get(`/notifications?${new URLSearchParams({ size: String(size) })}`));
|
|
78
|
+
registry.registerAuthenticated("list_my_liked_posts", "read", {
|
|
62
79
|
title: "List My Liked OpenLog Posts",
|
|
63
80
|
description: "Return posts liked by the authenticated OpenLog user.",
|
|
64
81
|
inputSchema: {
|
|
65
82
|
cursor: z.string().optional(),
|
|
66
83
|
size: z.number().int().min(1).max(20).default(10),
|
|
67
84
|
},
|
|
68
|
-
|
|
85
|
+
annotations: READ_TOOL_ANNOTATIONS,
|
|
86
|
+
}, (client, { cursor, size }) => {
|
|
69
87
|
const params = new URLSearchParams({ size: String(size) });
|
|
70
88
|
if (cursor) {
|
|
71
89
|
params.set("cursor", cursor);
|
|
72
90
|
}
|
|
73
|
-
return
|
|
91
|
+
return client.get(`/users/me/liked-posts?${params}`);
|
|
74
92
|
});
|
|
75
|
-
|
|
93
|
+
registry.registerAuthenticated("list_my_posts", "read", {
|
|
76
94
|
title: "List My OpenLog Posts",
|
|
77
95
|
description: "Return posts authored by the authenticated OpenLog user.",
|
|
78
96
|
inputSchema: {
|
|
79
97
|
size: z.number().int().min(1).max(100).default(20),
|
|
80
98
|
},
|
|
81
|
-
|
|
99
|
+
annotations: READ_TOOL_ANNOTATIONS,
|
|
100
|
+
}, async (client, { size }) => {
|
|
82
101
|
const me = await client.get("/auth/me");
|
|
83
102
|
const username = me.username?.trim();
|
|
84
103
|
if (!username) {
|
|
@@ -92,23 +111,22 @@ export async function runMcpServer() {
|
|
|
92
111
|
hasMore: posts.length > size,
|
|
93
112
|
posts: posts.slice(0, size),
|
|
94
113
|
};
|
|
95
|
-
})
|
|
96
|
-
|
|
114
|
+
});
|
|
115
|
+
registry.registerAuthenticated("upload_post_image", "write", {
|
|
97
116
|
title: "Upload OpenLog Post Image",
|
|
98
117
|
description: "Convert a local image file to WebP, upload it to OpenLog, and return markdown for post content.",
|
|
99
|
-
annotations: WRITE_TOOL_ANNOTATIONS,
|
|
100
118
|
inputSchema: {
|
|
101
119
|
filePath: z.string().min(1),
|
|
102
120
|
altText: z.string().optional(),
|
|
103
121
|
},
|
|
104
|
-
|
|
122
|
+
annotations: WRITE_TOOL_ANNOTATIONS,
|
|
123
|
+
}, (client, { filePath, altText }) => uploadImage(client, {
|
|
105
124
|
filePath,
|
|
106
125
|
altText,
|
|
107
|
-
}))
|
|
108
|
-
|
|
126
|
+
}));
|
|
127
|
+
registry.registerAuthenticated("publish_post", "publish", {
|
|
109
128
|
title: "Publish OpenLog Post",
|
|
110
|
-
description: "Publish a new post
|
|
111
|
-
annotations: WRITE_TOOL_ANNOTATIONS,
|
|
129
|
+
description: "Publish a new post. Requires confirm: true unless skipConfirmation: true is explicitly provided.",
|
|
112
130
|
inputSchema: {
|
|
113
131
|
title: z.string().min(1),
|
|
114
132
|
description: z.string().min(1),
|
|
@@ -123,7 +141,8 @@ export async function runMcpServer() {
|
|
|
123
141
|
confirm: z.boolean().optional(),
|
|
124
142
|
skipConfirmation: z.boolean().optional(),
|
|
125
143
|
},
|
|
126
|
-
|
|
144
|
+
annotations: WRITE_TOOL_ANNOTATIONS,
|
|
145
|
+
}, async (client, { title, description, content, topics, links, confirm, skipConfirmation, }) => {
|
|
127
146
|
const post = normalizePostInput({
|
|
128
147
|
title,
|
|
129
148
|
description,
|
|
@@ -131,6 +150,7 @@ export async function runMcpServer() {
|
|
|
131
150
|
topics,
|
|
132
151
|
links,
|
|
133
152
|
});
|
|
153
|
+
// 명시적 확인 전에는 POST를 실행하지 않고 실제 발행 입력의 미리보기만 반환한다.
|
|
134
154
|
if (confirm !== true && skipConfirmation !== true) {
|
|
135
155
|
return {
|
|
136
156
|
requiresConfirmation: true,
|
|
@@ -150,38 +170,30 @@ export async function runMcpServer() {
|
|
|
150
170
|
return {
|
|
151
171
|
...published,
|
|
152
172
|
path: postPath,
|
|
153
|
-
url: new URL(postPath, `${
|
|
173
|
+
url: new URL(postPath, `${webBaseUrl}/`).toString(),
|
|
154
174
|
};
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
title: "Push OpenLog Working Brief",
|
|
158
|
-
description: "Overwrite the workspace Now Working brief with a short status update: what was done, what is still open, and optional task/branch context. Latest write wins.",
|
|
159
|
-
annotations: {
|
|
160
|
-
...WRITE_TOOL_ANNOTATIONS,
|
|
161
|
-
idempotentHint: true,
|
|
162
|
-
},
|
|
163
|
-
inputSchema: {
|
|
164
|
-
workspaceId: z.number().int().positive(),
|
|
165
|
-
title: z.string().min(1).max(255),
|
|
166
|
-
prose: z.string().min(1),
|
|
167
|
-
taskId: z.number().int().positive().optional(),
|
|
168
|
-
branch: z.string().max(255).optional(),
|
|
169
|
-
},
|
|
170
|
-
}, async ({ workspaceId, title, prose, taskId, branch }) => withAuthenticatedClient((client) => client.put(`/workspaces/${workspaceId}/working-brief`, {
|
|
171
|
-
title,
|
|
172
|
-
prose,
|
|
173
|
-
taskId: taskId ?? null,
|
|
174
|
-
branch: branch ?? null,
|
|
175
|
-
})));
|
|
176
|
-
server.registerTool("get_post_detail", {
|
|
175
|
+
});
|
|
176
|
+
registry.registerAuthenticated("get_post_detail", "read", {
|
|
177
177
|
title: "Get OpenLog Post Detail",
|
|
178
178
|
description: "Return a public OpenLog post detail by author username and post slug.",
|
|
179
179
|
inputSchema: {
|
|
180
180
|
username: z.string().min(1),
|
|
181
181
|
slug: z.string().min(1),
|
|
182
182
|
},
|
|
183
|
-
|
|
184
|
-
|
|
183
|
+
annotations: READ_TOOL_ANNOTATIONS,
|
|
184
|
+
}, (client, { username, slug }) => client.get(`/users/${encodeURIComponent(username)}/posts/${encodeURIComponent(slug)}`));
|
|
185
|
+
// 워크스페이스 도구는 별도 catalog에서 등록해 서버 시작 안내와 테스트 목록을 일치시킨다.
|
|
186
|
+
registerWorkspaceTools(registry, webBaseUrl);
|
|
187
|
+
return {
|
|
188
|
+
server,
|
|
189
|
+
permissions,
|
|
190
|
+
toolNames: [...registry.toolNames],
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
export async function runMcpServer() {
|
|
194
|
+
const created = await createOpenLogMcpServer();
|
|
195
|
+
printMcpStartupHint(created.permissions, created.toolNames);
|
|
196
|
+
await created.server.connect(new StdioServerTransport());
|
|
185
197
|
}
|
|
186
198
|
function normalizePostInput(input) {
|
|
187
199
|
const title = input.title.trim();
|
|
@@ -225,38 +237,22 @@ function normalizeLinks(links) {
|
|
|
225
237
|
}
|
|
226
238
|
return normalized;
|
|
227
239
|
}
|
|
228
|
-
function createContentPreview(content) {
|
|
229
|
-
const preview = content.replace(/\s+/g, " ").trim();
|
|
230
|
-
return preview.length > 500 ? `${preview.slice(0, 497)}...` : preview;
|
|
231
|
-
}
|
|
232
240
|
function buildPublicPostPath(username, slug) {
|
|
233
241
|
return `/@${encodeURIComponent(username)}/posts/${encodeURIComponent(slug)}`;
|
|
234
242
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const result = await callback(client);
|
|
239
|
-
return textResult(result);
|
|
240
|
-
}
|
|
241
|
-
catch (error) {
|
|
242
|
-
return {
|
|
243
|
-
content: [
|
|
244
|
-
{
|
|
245
|
-
type: "text",
|
|
246
|
-
text: error instanceof Error ? error.message : String(error),
|
|
247
|
-
},
|
|
248
|
-
],
|
|
249
|
-
isError: true,
|
|
250
|
-
};
|
|
243
|
+
function printMcpStartupHint(permissions, toolNames) {
|
|
244
|
+
if (!process.stderr.isTTY) {
|
|
245
|
+
return;
|
|
251
246
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
247
|
+
console.error(`OpenLog MCP server is running over stdio.
|
|
248
|
+
|
|
249
|
+
Profile: ${permissions.profile}
|
|
250
|
+
Capabilities: ${permissions.capabilities.join(", ")}
|
|
251
|
+
|
|
252
|
+
This terminal is now reserved for MCP protocol traffic.
|
|
253
|
+
Press Ctrl+C to stop it.
|
|
254
|
+
|
|
255
|
+
Available tools:
|
|
256
|
+
${toolNames.map((name) => ` ${name}`).join("\n")}
|
|
257
|
+
`);
|
|
262
258
|
}
|