@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 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 tools:
12
-
13
- - `get_auth_status`
14
- - `get_me`
15
- - `list_my_notifications`
16
- - `list_my_posts`
17
- - `list_my_liked_posts`
18
- - `upload_post_image`
19
- - `publish_post`
20
- - `push_working_brief`
21
- - `get_post_detail`
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.
@@ -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
- return (await response.json());
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) {
@@ -28,7 +28,8 @@ Aliases:
28
28
  claude Same as claude-code
29
29
 
30
30
  Notes:
31
- If OPENLOG_API_BASE_URL or OPENLOG_WEB_BASE_URL is set, it is added to the client config.
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
+ }
@@ -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
- const WRITE_TOOL_ANNOTATIONS = {
9
- readOnlyHint: false,
10
- destructiveHint: false,
11
- idempotentHint: false,
12
- openWorldHint: true,
13
- };
14
- export async function runMcpServer() {
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.registerTool("get_auth_status", {
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 apiBaseUrl = getApiBaseUrl();
39
+ let resolvedApiBaseUrl = apiBaseUrl;
25
40
  try {
26
- const authFile = await readAuthFile();
41
+ const authFile = await readAuth();
27
42
  if (!authFile) {
28
- return textResult({
43
+ return {
29
44
  authenticated: false,
30
- apiBaseUrl,
31
- });
45
+ apiBaseUrl: resolvedApiBaseUrl,
46
+ };
32
47
  }
33
- apiBaseUrl = authFile.apiBaseUrl;
34
- const me = await createAuthenticatedApiClient().then((client) => client.get("/auth/me"));
35
- return textResult({
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 textResult({
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
- server.registerTool("get_me", {
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
- }, async () => withAuthenticatedClient((client) => client.get("/auth/me")));
54
- server.registerTool("list_my_notifications", {
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
- }, async ({ size }) => withAuthenticatedClient((client) => client.get(`/notifications?${new URLSearchParams({ size: String(size) })}`)));
61
- server.registerTool("list_my_liked_posts", {
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
- }, async ({ cursor, size }) => {
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 withAuthenticatedClient((client) => client.get(`/users/me/liked-posts?${params}`));
91
+ return client.get(`/users/me/liked-posts?${params}`);
74
92
  });
75
- server.registerTool("list_my_posts", {
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
- }, async ({ size }) => withAuthenticatedClient(async (client) => {
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
- server.registerTool("upload_post_image", {
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
- }, async ({ filePath, altText }) => withAuthenticatedClient((client) => uploadPostImage(client, {
122
+ annotations: WRITE_TOOL_ANNOTATIONS,
123
+ }, (client, { filePath, altText }) => uploadImage(client, {
105
124
  filePath,
106
125
  altText,
107
- })));
108
- server.registerTool("publish_post", {
126
+ }));
127
+ registry.registerAuthenticated("publish_post", "publish", {
109
128
  title: "Publish OpenLog Post",
110
- description: "Publish a new post to the authenticated OpenLog account. Requires confirm: true unless skipConfirmation: true is explicitly provided.",
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
- }, async ({ title, description, content, topics, links, confirm, skipConfirmation, }) => withAuthenticatedClient(async (client) => {
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, `${getWebBaseUrl()}/`).toString(),
173
+ url: new URL(postPath, `${webBaseUrl}/`).toString(),
154
174
  };
155
- }));
156
- server.registerTool("push_working_brief", {
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
- }, async ({ username, slug }) => withAuthenticatedClient((client) => client.get(`/users/${encodeURIComponent(username)}/posts/${encodeURIComponent(slug)}`)));
184
- await server.connect(new StdioServerTransport());
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
- async function withAuthenticatedClient(callback) {
236
- try {
237
- const client = await createAuthenticatedApiClient();
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
- function textResult(value) {
254
- return {
255
- content: [
256
- {
257
- type: "text",
258
- text: typeof value === "string" ? value : JSON.stringify(value, null, 2),
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
  }