Skip to main content

max / goingson

Remove MCP server and LLM integration Remove goingson-mcp crate, LLM client/commands/templates, and llm_repo from db-sqlite. Clean up workspace deps and CSS. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-04-16 01:38 UTC
Commit: c4f0b1fea08b04729a53a7f253e3f0acc808f5e1
Parent: 7516548
41 files changed, +10 insertions, -5689 deletions
M Cargo.lock -57
@@ -1943,25 +1943,6 @@ dependencies = [
1943 1943 ]
1944 1944
1945 1945 [[package]]
1946 - name = "goingson-mcp"
1947 - version = "0.3.1"
1948 - dependencies = [
1949 - "chrono",
1950 - "dirs",
1951 - "goingson-core",
1952 - "goingson-db-sqlite",
1953 - "rmcp",
1954 - "schemars 0.8.22",
1955 - "serde",
1956 - "serde_json",
1957 - "sqlx",
1958 - "tokio",
1959 - "tracing",
1960 - "tracing-subscriber",
1961 - "uuid",
1962 - ]
1963 -
1964 - [[package]]
1965 1946 name = "goingson-plugin-runtime"
1966 1947 version = "0.3.1"
1967 1948 dependencies = [
@@ -3647,12 +3628,6 @@ dependencies = [
3647 3628 ]
3648 3629
3649 3630 [[package]]
3650 - name = "paste"
3651 - version = "1.0.15"
3652 - source = "registry+https://github.com/rust-lang/crates.io-index"
3653 - checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
3654 -
3655 - [[package]]
3656 3631 name = "pathdiff"
3657 3632 version = "0.2.3"
3658 3633 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4526,38 +4501,6 @@ dependencies = [
4526 4501 ]
4527 4502
4528 4503 [[package]]
4529 - name = "rmcp"
4530 - version = "0.1.5"
4531 - source = "registry+https://github.com/rust-lang/crates.io-index"
4532 - checksum = "33a0110d28bd076f39e14bfd5b0340216dd18effeb5d02b43215944cc3e5c751"
4533 - dependencies = [
4534 - "base64 0.21.7",
4535 - "chrono",
4536 - "futures",
4537 - "paste",
4538 - "pin-project-lite",
4539 - "rmcp-macros",
4540 - "schemars 0.8.22",
4541 - "serde",
4542 - "serde_json",
4543 - "thiserror 2.0.18",
4544 - "tokio",
4545 - "tokio-util",
4546 - "tracing",
4547 - ]
4548 -
4549 - [[package]]
4550 - name = "rmcp-macros"
4551 - version = "0.1.5"
4552 - source = "registry+https://github.com/rust-lang/crates.io-index"
4553 - checksum = "a6e2b2fd7497540489fa2db285edd43b7ed14c49157157438664278da6e42a7a"
4554 - dependencies = [
4555 - "proc-macro2",
4556 - "quote",
4557 - "syn 2.0.117",
4558 - ]
4559 -
4560 - [[package]]
4561 4504 name = "rsa"
4562 4505 version = "0.9.10"
4563 4506 source = "registry+https://github.com/rust-lang/crates.io-index"
M Cargo.toml -1
@@ -2,7 +2,6 @@
2 2 members = [
3 3 "crates/core",
4 4 "crates/db-sqlite",
5 - "crates/goingson-mcp",
6 5 "crates/plugin-runtime",
7 6 "src-tauri",
8 7 ]
@@ -1,114 +0,0 @@
1 - //! LLM provider types and DTOs.
2 -
3 - use chrono::{DateTime, Utc};
4 - use serde::{Deserialize, Serialize};
5 - use strum_macros::EnumString;
6 - use crate::id_types::{LlmSettingsId, UserId};
7 -
8 - use super::shared::DbValue;
9 -
10 - // ============ LLM Types ============
11 -
12 - /// Supported LLM provider types.
13 - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, EnumString)]
14 - pub enum LlmProviderType {
15 - /// Local Ollama instance.
16 - #[strum(serialize = "ollama")]
17 - #[default]
18 - Ollama,
19 - /// OpenAI-compatible API (OpenAI, Groq, etc.).
20 - #[strum(serialize = "openai_compatible")]
21 - OpenAiCompatible,
22 - }
23 -
24 - impl LlmProviderType {
25 - /// Returns a human-readable display string.
26 - pub fn as_str(&self) -> &'static str {
27 - match self {
28 - LlmProviderType::Ollama => "Ollama",
29 - LlmProviderType::OpenAiCompatible => "OpenAI Compatible",
30 - }
31 - }
32 -
33 - /// Parses a string into an LlmProviderType, falling back to `Ollama` on invalid input.
34 - ///
35 - /// Accepts various formats: "ollama"/"Ollama", "openai_compatible"/"OpenAiCompatible"/"openai"/"OpenAI".
36 - /// This intentional fallback ensures database reads and frontend input never fail.
37 - /// Use `str.parse::<LlmProviderType>()` if you need error handling.
38 - #[allow(clippy::should_implement_trait)]
39 - pub fn from_str_or_default(s: &str) -> Self {
40 - match s {
41 - "ollama" | "Ollama" => LlmProviderType::Ollama,
42 - "openai_compatible" | "OpenAiCompatible" | "openai" | "OpenAI" => LlmProviderType::OpenAiCompatible,
43 - _ => LlmProviderType::default(),
44 - }
45 - }
46 - }
47 -
48 - impl DbValue for LlmProviderType {
49 - fn db_value(&self) -> &'static str {
50 - match self {
51 - LlmProviderType::Ollama => "ollama",
52 - LlmProviderType::OpenAiCompatible => "openai_compatible",
53 - }
54 - }
55 - }
56 -
57 - /// User's LLM provider configuration.
58 - #[derive(Debug, Clone, Serialize, Deserialize)]
59 - pub struct LlmSettings {
60 - /// Unique identifier.
61 - pub id: LlmSettingsId,
62 - /// Owner user ID.
63 - pub user_id: UserId,
64 - /// Provider type.
65 - pub provider_type: LlmProviderType,
66 - /// API base URL.
67 - pub base_url: String,
68 - /// API key (if required).
69 - pub api_key: Option<String>,
70 - /// Model name/identifier.
71 - pub model_name: String,
72 - /// Request timeout in milliseconds.
73 - pub timeout_ms: i32,
74 - /// Maximum tokens in response.
75 - pub max_tokens: i32,
76 - /// Sampling temperature (0.0 to 1.0).
77 - pub temperature: f64,
78 - /// Whether LLM features are enabled.
79 - pub is_enabled: bool,
80 - /// When settings were created.
81 - pub created_at: DateTime<Utc>,
82 - /// When settings were last modified.
83 - pub updated_at: DateTime<Utc>,
84 - }
85 -
86 - /// Data for creating or updating LLM settings.
87 - #[derive(Debug, Clone, Serialize, Deserialize)]
88 - pub struct NewLlmSettings {
89 - pub provider_type: LlmProviderType,
90 - pub base_url: String,
91 - pub api_key: Option<String>,
92 - pub model_name: String,
93 - pub timeout_ms: i32,
94 - pub max_tokens: i32,
95 - pub temperature: f64,
96 - pub is_enabled: bool,
97 - }
98 -
99 - /// Temporal context passed to LLM prompts.
100 - #[derive(Debug, Clone, Serialize, Deserialize)]
101 - pub struct LlmContext {
102 - /// Current date (YYYY-MM-DD).
103 - pub today: String,
104 - /// Day of year (1-366).
105 - pub day_of_year: i32,
106 - /// Day name ("Monday", etc.).
107 - pub day_of_week: String,
108 - /// ISO week number.
109 - pub week_of_year: i32,
110 - /// Month (1-12).
111 - pub month: i32,
112 - /// Year.
113 - pub year: i32,
114 - }
@@ -5,7 +5,6 @@ mod backup;
5 5 mod email;
6 6 mod email_account;
7 7 mod event;
8 - mod llm;
9 8 mod milestone;
10 9 mod project;
11 10 mod saved_view;
@@ -22,7 +21,6 @@ pub use backup::*;
22 21 pub use email::*;
23 22 pub use email_account::*;
24 23 pub use event::*;
25 - pub use llm::*;
26 24 pub use milestone::*;
27 25 pub use monthly_review::*;
28 26 pub use project::*;
@@ -19,8 +19,8 @@ use crate::contact::{
19 19 };
20 20 use crate::error::CoreError;
21 21 use crate::models::{
22 - Annotation, Attachment, Email, EmailAccount, EmailAuthType, EmailThread, Event, LlmSettings,
23 - NewAttachment, NewEmail, NewEmailWithTracking, NewEvent, NewLlmSettings, NewProject,
22 + Annotation, Attachment, Email, EmailAccount, EmailAuthType, EmailThread, Event,
23 + NewAttachment, NewEmail, NewEmailWithTracking, NewEvent, NewProject,
24 24 NewSavedView, NewTask, Project, SavedView, Subtask, Task, TaskFilterQuery, TimeSession,
25 25 TimeTrackingSummary, UpdateTask, User,
26 26 };
@@ -660,44 +660,6 @@ pub trait SearchRepository: Send + Sync {
660 660 async fn search(&self, user_id: UserId, query: SearchQuery) -> Result<(Vec<SearchResultItem>, usize)>;
661 661 }
662 662
663 - /// Repository for LLM provider settings.
664 - #[async_trait]
665 - pub trait LlmSettingsRepository: Send + Sync {
666 - /// Gets the LLM settings for a user.
667 - async fn get(&self, user_id: UserId) -> Result<Option<LlmSettings>>;
668 -
669 - /// Creates or updates LLM settings.
670 - async fn upsert(&self, user_id: UserId, settings: NewLlmSettings) -> Result<LlmSettings>;
671 - }
672 -
673 - /// Repository for caching LLM responses.
674 - ///
675 - /// Caches are keyed by prompt hash and optional context date to support
676 - /// date-sensitive queries (e.g., "what's due today?").
677 - #[async_trait]
678 - pub trait LlmCacheRepository: Send + Sync {
679 - /// Gets a cached response by prompt hash and context date.
680 - async fn get(
681 - &self,
682 - user_id: UserId,
683 - prompt_hash: &str,
684 - context_date: Option<&str>,
685 - ) -> Result<Option<String>>;
686 -
687 - /// Stores a response in the cache.
688 - async fn set(
689 - &self,
690 - user_id: UserId,
691 - prompt_hash: &str,
692 - context_date: Option<&str>,
693 - response: &str,
694 - expires_at: Option<DateTime<Utc>>,
695 - ) -> Result<()>;
696 -
697 - /// Clears all cached responses for a user, returning the count deleted.
698 - async fn clear_all(&self, user_id: UserId) -> Result<u64>;
699 - }
700 -
701 663 /// Repository for saved view / filter configurations.
702 664 #[async_trait]
703 665 pub trait SavedViewRepository: Send + Sync {
@@ -56,8 +56,6 @@ pub use repository::{
56 56 SqliteEmailAccountRepository,
57 57 SqliteStatsRepository,
58 58 SqliteSearchRepository,
59 - SqliteLlmSettingsRepository,
60 - SqliteLlmCacheRepository,
61 59 SqliteMilestoneRepository,
62 60 SqliteMonthlyReviewRepository,
63 61 SqliteSavedViewRepository,
@@ -1,253 +0,0 @@
1 - //! SQLite implementations of LLM-related repositories.
2 - //!
3 - //! Manages LLM provider settings (API keys, model configs, timeouts) and
4 - //! caches LLM responses to reduce API calls and improve response times.
5 -
6 - use async_trait::async_trait;
7 - use chrono::{DateTime, Utc};
8 - use sqlx::SqlitePool;
9 - use uuid::Uuid;
10 -
11 - use goingson_core::{
12 - CoreError, DbValue, LlmCacheRepository, LlmProviderType, LlmSettings, LlmSettingsId,
13 - LlmSettingsRepository, NewLlmSettings, Result, UserId,
14 - };
15 -
16 - use crate::utils::{format_datetime_now, format_datetime_opt, parse_datetime, parse_uuid};
17 -
18 - // ============ LLM Settings ============
19 -
20 - #[derive(Debug, Clone, sqlx::FromRow)]
21 - struct LlmSettingsRow {
22 - pub id: String,
23 - pub user_id: String,
24 - pub provider_type: String,
25 - pub base_url: String,
26 - pub api_key: Option<String>,
27 - pub model_name: String,
28 - pub timeout_ms: i32,
29 - pub max_tokens: i32,
30 - pub temperature: f64,
31 - pub is_enabled: i32,
32 - pub created_at: String,
33 - pub updated_at: String,
34 - }
35 -
36 - impl TryFrom<LlmSettingsRow> for LlmSettings {
37 - type Error = CoreError;
38 -
39 - fn try_from(row: LlmSettingsRow) -> std::result::Result<Self, Self::Error> {
40 - Ok(LlmSettings {
41 - id: parse_uuid(&row.id)?.into(),
42 - user_id: parse_uuid(&row.user_id)?.into(),
43 - provider_type: LlmProviderType::from_str_or_default(&row.provider_type),
44 - base_url: row.base_url,
45 - api_key: row.api_key,
46 - model_name: row.model_name,
47 - timeout_ms: row.timeout_ms,
48 - max_tokens: row.max_tokens,
49 - temperature: row.temperature,
50 - is_enabled: row.is_enabled != 0,
51 - created_at: parse_datetime(&row.created_at)?,
52 - updated_at: parse_datetime(&row.updated_at)?,
53 - })
54 - }
55 - }
56 -
57 - /// SQLite-backed implementation of [`LlmSettingsRepository`].
58 - ///
59 - /// Stores LLM provider configuration (API keys, model selection, parameters)
60 - /// with one settings record per user.
61 - pub struct SqliteLlmSettingsRepository {
62 - pool: SqlitePool,
63 - }
64 -
65 - impl SqliteLlmSettingsRepository {
66 - /// Creates a new repository instance with the given connection pool.
67 - pub fn new(pool: SqlitePool) -> Self {
68 - Self { pool }
69 - }
70 - }
71 -
72 - #[async_trait]
73 - impl LlmSettingsRepository for SqliteLlmSettingsRepository {
74 - async fn get(&self, user_id: UserId) -> Result<Option<LlmSettings>> {
75 - let row = sqlx::query_as::<_, LlmSettingsRow>(
76 - r#"SELECT id, user_id, provider_type, base_url, api_key, model_name,
77 - timeout_ms, max_tokens, temperature, is_enabled, created_at, updated_at
78 - FROM llm_settings WHERE user_id = ?"#,
79 - )
80 - .bind(user_id.to_string())
81 - .fetch_optional(&self.pool)
82 - .await
83 - .map_err(CoreError::database)?;
84 -
85 - row.map(LlmSettings::try_from).transpose()
86 - }
87 -
88 - async fn upsert(&self, user_id: UserId, settings: NewLlmSettings) -> Result<LlmSettings> {
89 - let now = format_datetime_now();
90 -
91 - // Check if settings exist
92 - let existing = self.get(user_id).await?;
93 -
94 - if existing.is_some() {
95 - // Update
96 - sqlx::query(
97 - r#"UPDATE llm_settings SET
98 - provider_type = ?, base_url = ?, api_key = ?, model_name = ?,
99 - timeout_ms = ?, max_tokens = ?, temperature = ?, is_enabled = ?, updated_at = ?
100 - WHERE user_id = ?"#,
101 - )
102 - .bind(settings.provider_type.db_value())
103 - .bind(&settings.base_url)
104 - .bind(&settings.api_key)
105 - .bind(&settings.model_name)
106 - .bind(settings.timeout_ms)
107 - .bind(settings.max_tokens)
108 - .bind(settings.temperature)
109 - .bind(if settings.is_enabled { 1 } else { 0 })
110 - .bind(&now)
111 - .bind(user_id.to_string())
112 - .execute(&self.pool)
113 - .await
114 - .map_err(CoreError::database)?;
115 -
116 - self.get(user_id)
117 - .await?
118 - .ok_or_else(|| CoreError::internal("Failed to retrieve updated settings"))
119 - } else {
120 - // Insert
121 - let id = LlmSettingsId::new();
122 - sqlx::query(
123 - r#"INSERT INTO llm_settings
124 - (id, user_id, provider_type, base_url, api_key, model_name,
125 - timeout_ms, max_tokens, temperature, is_enabled, created_at, updated_at)
126 - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#,
127 - )
128 - .bind(id.to_string())
129 - .bind(user_id.to_string())
130 - .bind(settings.provider_type.db_value())
131 - .bind(&settings.base_url)
132 - .bind(&settings.api_key)
133 - .bind(&settings.model_name)
134 - .bind(settings.timeout_ms)
135 - .bind(settings.max_tokens)
136 - .bind(settings.temperature)
137 - .bind(if settings.is_enabled { 1 } else { 0 })
138 - .bind(&now)
139 - .bind(&now)
140 - .execute(&self.pool)
141 - .await
142 - .map_err(CoreError::database)?;
143 -
144 - self.get(user_id)
145 - .await?
146 - .ok_or_else(|| CoreError::internal("Failed to retrieve created settings"))
147 - }
148 - }
149 - }
150 -
151 - // ============ LLM Cache ============
152 -
153 - #[derive(Debug, Clone, sqlx::FromRow)]
154 - struct LlmCacheRow {
155 - pub response: String,
156 - }
157 -
158 - /// SQLite-backed implementation of [`LlmCacheRepository`].
159 - ///
160 - /// Caches LLM responses keyed by prompt hash and optional context date.
161 - /// Supports automatic expiration for time-sensitive cached responses.
162 - pub struct SqliteLlmCacheRepository {
163 - pool: SqlitePool,
164 - }
165 -
166 - impl SqliteLlmCacheRepository {
167 - /// Creates a new repository instance with the given connection pool.
168 - pub fn new(pool: SqlitePool) -> Self {
169 - Self { pool }
170 - }
171 - }
172 -
173 - #[async_trait]
174 - impl LlmCacheRepository for SqliteLlmCacheRepository {
175 - async fn get(
176 - &self,
177 - user_id: UserId,
178 - prompt_hash: &str,
179 - context_date: Option<&str>,
180 - ) -> Result<Option<String>> {
181 - let now = format_datetime_now();
182 -
183 - let row = if let Some(date) = context_date {
184 - sqlx::query_as::<_, LlmCacheRow>(
185 - r#"SELECT response FROM llm_cache
186 - WHERE user_id = ? AND prompt_hash = ? AND context_date = ?
187 - AND (expires_at IS NULL OR expires_at > ?)"#,
188 - )
189 - .bind(user_id.to_string())
190 - .bind(prompt_hash)
191 - .bind(date)
192 - .bind(&now)
193 - .fetch_optional(&self.pool)
194 - .await
195 - } else {
196 - sqlx::query_as::<_, LlmCacheRow>(
197 - r#"SELECT response FROM llm_cache
198 - WHERE user_id = ? AND prompt_hash = ? AND context_date IS NULL
199 - AND (expires_at IS NULL OR expires_at > ?)"#,
200 - )
201 - .bind(user_id.to_string())
202 - .bind(prompt_hash)
203 - .bind(&now)
204 - .fetch_optional(&self.pool)
205 - .await
206 - };
207 -
208 - row.map_err(CoreError::database)
209 - .map(|r| r.map(|row| row.response))
210 - }
211 -
212 - async fn set(
213 - &self,
214 - user_id: UserId,
215 - prompt_hash: &str,
216 - context_date: Option<&str>,
217 - response: &str,
218 - expires_at: Option<DateTime<Utc>>,
219 - ) -> Result<()> {
220 - let id = Uuid::new_v4();
221 - let now = format_datetime_now();
222 - let expires = format_datetime_opt(expires_at);
223 -
224 - // Use INSERT OR REPLACE to handle unique constraint
225 - sqlx::query(
226 - r#"INSERT OR REPLACE INTO llm_cache
227 - (id, user_id, prompt_hash, context_date, response, created_at, expires_at)
228 - VALUES (?, ?, ?, ?, ?, ?, ?)"#,
229 - )
230 - .bind(id.to_string())
231 - .bind(user_id.to_string())
232 - .bind(prompt_hash)
233 - .bind(context_date)
234 - .bind(response)
235 - .bind(&now)
236 - .bind(expires)
237 - .execute(&self.pool)
238 - .await
239 - .map_err(CoreError::database)?;
240 -
241 - Ok(())
242 - }
243 -
244 - async fn clear_all(&self, user_id: UserId) -> Result<u64> {
245 - let result = sqlx::query("DELETE FROM llm_cache WHERE user_id = ?")
246 - .bind(user_id.to_string())
247 - .execute(&self.pool)
248 - .await
249 - .map_err(CoreError::database)?;
250 -
251 - Ok(result.rows_affected())
252 - }
253 - }
@@ -18,7 +18,6 @@ mod user_repo;
18 18 mod email_account_repo;
19 19 mod stats_repo;
20 20 mod search_repo;
21 - mod llm_repo;
22 21 mod saved_view_repo;
23 22 mod milestone_repo;
24 23 mod monthly_review_repo;
@@ -36,7 +35,6 @@ pub use user_repo::SqliteUserRepository;
36 35 pub use email_account_repo::SqliteEmailAccountRepository;
37 36 pub use stats_repo::SqliteStatsRepository;
38 37 pub use search_repo::SqliteSearchRepository;
39 - pub use llm_repo::{SqliteLlmSettingsRepository, SqliteLlmCacheRepository};
40 38 pub use saved_view_repo::SqliteSavedViewRepository;
41 39 pub use milestone_repo::SqliteMilestoneRepository;
42 40 pub use monthly_review_repo::SqliteMonthlyReviewRepository;
@@ -1,37 +0,0 @@
1 - [package]
2 - name = "goingson-mcp"
3 - version.workspace = true
4 - edition.workspace = true
5 -
6 - [lib]
7 - name = "goingson_mcp"
8 - path = "src/lib.rs"
9 -
10 - [[bin]]
11 - name = "goingson-mcp"
12 - path = "src/main.rs"
13 -
14 - [dependencies]
15 - goingson-core = { workspace = true }
16 - goingson-db-sqlite = { workspace = true }
17 -
18 - # MCP protocol
19 - rmcp = { version = "0.1", features = ["server", "transport-io"] }
20 -
21 - # Async runtime
22 - tokio = { workspace = true, features = ["io-std", "io-util"] }
23 -
24 - # Serialization
25 - serde = { workspace = true }
26 - serde_json = { workspace = true }
27 - schemars = "0.8"
28 -
29 - # Database
30 - sqlx = { workspace = true, features = ["sqlite"] }
31 -
32 - # Utilities
33 - chrono = { workspace = true }
34 - uuid = { workspace = true }
35 - dirs = { workspace = true }
36 - tracing = { workspace = true }
37 - tracing-subscriber = { workspace = true }
@@ -1,6 +0,0 @@
1 - //! GoingsOn MCP Server library.
2 - //!
3 - //! Re-exports the core server types for integration tests.
4 -
5 - pub mod state;
6 - pub mod tools;
@@ -1,68 +0,0 @@
1 - //! GoingsOn MCP Server
2 - //!
3 - //! A standalone MCP (Model Context Protocol) server that allows Claude Code
4 - //! to manage tasks in GoingsOn.
5 - //!
6 - //! # Usage
7 - //!
8 - //! ```bash
9 - //! goingson-mcp # uses installed app database (com.goingson.app)
10 - //! goingson-mcp --dev # uses standalone MCP database (com.goingson.desktop)
11 - //! GOINGSON_DB=/path/to/db goingson-mcp # explicit database path
12 - //! ```
13 - //!
14 - //! Configure Claude Code (~/.claude/mcp.json):
15 - //! ```json
16 - //! {
17 - //! "mcpServers": {
18 - //! "goingson": {
19 - //! "command": "/path/to/goingson-mcp",
20 - //! "args": []
21 - //! }
22 - //! }
23 - //! }
24 - //! ```
25 -
26 - use rmcp::ServiceExt;
27 - use tokio::io::{stdin, stdout};
28 - use tracing::info;
29 - use tracing_subscriber::{fmt, prelude::*, EnvFilter};
30 -
31 - use goingson_mcp::state::{DbMode, McpState};
32 - use goingson_mcp::tools::GoingsOnServer;
33 -
34 - #[tokio::main]
35 - async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
36 - // Initialize logging to stderr (stdout is used for MCP communication)
37 - tracing_subscriber::registry()
38 - .with(fmt::layer().with_writer(std::io::stderr))
39 - .with(EnvFilter::from_default_env().add_directive("goingson_mcp=info".parse()?))
40 - .init();
41 -
42 - let mode = if std::env::args().any(|a| a == "--dev") {
43 - DbMode::Dev
44 - } else {
45 - DbMode::App
46 - };
47 -
48 - info!(?mode, "Starting GoingsOn MCP server");
49 -
50 - // Initialize database connection
51 - let state = McpState::new(mode).await?;
52 -
53 - // Create MCP server
54 - let server = GoingsOnServer::new(state);
55 -
56 - // Build stdio transport
57 - let transport = (stdin(), stdout());
58 -
59 - // Serve MCP protocol
60 - info!("MCP server ready, waiting for connections...");
61 - let service = server.serve(transport).await?;
62 -
63 - // Wait for shutdown
64 - let quit_reason = service.waiting().await?;
65 - info!(?quit_reason, "MCP server shutting down");
66 -
67 - Ok(())
68 - }
@@ -1,137 +0,0 @@
1 - //! Database connection and repository state for MCP server.
2 - //!
3 - //! Connects to the GoingsOn SQLite database. By default uses the installed
4 - //! app's database (`com.goingson.app`). Pass `--dev` to use a standalone
5 - //! MCP database (`com.goingson.desktop`), or set `GOINGSON_DB` to an
6 - //! explicit path.
7 -
8 - use goingson_core::{ContactRepository, EventRepository, MilestoneRepository, ProjectRepository, StatsRepository, TaskRepository, UserId};
9 - use goingson_db_sqlite::{SqliteContactRepository, SqliteEventRepository, SqliteMilestoneRepository, SqliteProjectRepository, SqliteStatsRepository, SqliteTaskRepository};
10 - use sqlx::SqlitePool;
11 - use std::path::PathBuf;
12 - use std::sync::Arc;
13 - use tracing::{debug, info};
14 -
15 - /// Fixed user ID for single-user desktop app (matches Tauri app)
16 - pub const DESKTOP_USER_ID: UserId = UserId::from_uuid(uuid::Uuid::from_u128(1));
17 -
18 - /// Which database the MCP server should connect to.
19 - #[derive(Debug, Clone, Copy, PartialEq, Eq)]
20 - pub enum DbMode {
21 - /// Installed Tauri app database (com.goingson.app)
22 - App,
23 - /// Standalone MCP development database (com.goingson.desktop)
24 - Dev,
25 - }
26 -
27 - /// Application state with database connection and repositories
28 - #[derive(Clone)]
29 - pub struct McpState {
30 - pub tasks: Arc<dyn TaskRepository>,
31 - pub projects: Arc<dyn ProjectRepository>,
32 - pub events: Arc<dyn EventRepository>,
33 - pub milestones: Arc<dyn MilestoneRepository>,
34 - pub contacts: Arc<dyn ContactRepository>,
35 - pub stats: Arc<dyn StatsRepository>,
36 - }
37 -
38 - impl McpState {
39 - /// Creates new state by connecting to the GoingsOn database.
40 - ///
41 - /// Database resolution order:
42 - /// 1. `GOINGSON_DB` env var (explicit path)
43 - /// 2. `--dev` flag → `com.goingson.desktop`
44 - /// 3. Default → `com.goingson.app` (installed Tauri app)
45 - pub async fn new(mode: DbMode) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
46 - let db_path = get_database_path(mode)?;
47 - info!(?db_path, ?mode, "Connecting to GoingsOn database");
48 -
49 - let db_url = format!("sqlite:{}?mode=rwc", db_path.display());
50 - let pool = SqlitePool::connect(&db_url).await?;
51 -
52 - // Run migrations to ensure schema is up to date
53 - sqlx::migrate!("../../migrations/sqlite")
54 - .run(&pool)
55 - .await?;
56 -
57 - info!("Database connection established");
58 -
59 - // Ensure desktop user exists (single-user mode)
60 - ensure_desktop_user_exists(&pool).await?;
61 -
62 - let tasks = Arc::new(SqliteTaskRepository::new(pool.clone()));
63 - let projects = Arc::new(SqliteProjectRepository::new(pool.clone()));
64 - let events = Arc::new(SqliteEventRepository::new(pool.clone()));
65 - let milestones = Arc::new(SqliteMilestoneRepository::new(pool.clone()));
66 - let contacts = Arc::new(SqliteContactRepository::new(pool.clone()));
67 - let stats = Arc::new(SqliteStatsRepository::new(pool));
68 -
69 - Ok(Self { tasks, projects, events, milestones, contacts, stats })
70 - }
71 - }
72 -
73 - /// Gets the path to the GoingsOn database.
74 - ///
75 - /// Resolution order:
76 - /// 1. `GOINGSON_DB` env var → use that path directly
77 - /// 2. `DbMode::Dev` → `<data_dir>/com.goingson.desktop/goingson.db`
78 - /// 3. `DbMode::App` → `<data_dir>/com.goingson.app/goingson.db`
79 - ///
80 - /// Data dir locations:
81 - /// - macOS: ~/Library/Application Support/
82 - /// - Linux: ~/.local/share/
83 - /// - Windows: %APPDATA%/
84 - fn get_database_path(mode: DbMode) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
85 - // Explicit override via env var
86 - if let Ok(explicit) = std::env::var("GOINGSON_DB") {
87 - let path = PathBuf::from(&explicit);
88 - if let Some(parent) = path.parent() {
89 - std::fs::create_dir_all(parent)?;
90 - }
91 - info!(path = %path.display(), "Using GOINGSON_DB override");
92 - return Ok(path);
93 - }
94 -
95 - let data_dir = dirs::data_dir()
96 - .ok_or("Could not determine data directory")?;
97 -
98 - let dir_name = match mode {
99 - DbMode::App => "com.goingson.app",
100 - DbMode::Dev => "com.goingson.desktop",
101 - };
102 -
103 - let app_dir = data_dir.join(dir_name);
104 - std::fs::create_dir_all(&app_dir)?;
105 -
106 - Ok(app_dir.join("goingson.db"))
107 - }
108 -
109 - /// Ensure the desktop user exists in the database
110 - async fn ensure_desktop_user_exists(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
111 - let user_id = DESKTOP_USER_ID.to_string();
112 -
113 - // Check if user already exists
114 - let exists: Option<(String,)> = sqlx::query_as("SELECT id FROM users WHERE id = ?")
115 - .bind(&user_id)
116 - .fetch_optional(pool)
117 - .await?;
118 -
119 - if exists.is_none() {
120 - info!("Creating desktop user");
121 - let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
122 - sqlx::query(
123 - "INSERT INTO users (id, email, password_hash, display_name, created_at) VALUES (?, ?, ?, ?, ?)"
124 - )
125 - .bind(&user_id)
126 - .bind("desktop@localhost")
127 - .bind("desktop-mode-no-password")
128 - .bind("Desktop User")
129 - .bind(&now)
130 - .execute(pool)
131 - .await?;
132 - } else {
133 - debug!("Desktop user already exists");
134 - }
135 -
136 - Ok(())
137 - }
@@ -1,267 +0,0 @@
1 - //! Contact implementation methods for GoingsOnServer.
2 -
3 - use chrono::NaiveDate;
4 - use tracing::instrument;
5 -
6 - use goingson_core::{
7 - ContactId, ContactEmailId, ContactPhoneId,
8 - NewContact, UpdateContact, NewContactEmail, NewContactPhone,
9 - NewSocialHandle, NewContactCustomField,
10 - };
11 -
12 - use crate::state::DESKTOP_USER_ID;
13 -
14 - use super::GoingsOnServer;
15 - use super::results::ContactResult;
16 - use super::contact_params::*;
17 -
18 - impl GoingsOnServer {
19 - pub(crate) fn contact_to_result(contact: &goingson_core::Contact) -> ContactResult {
20 - ContactResult {
21 - id: contact.id.to_string(),
22 - display_name: contact.display_name.clone(),
23 - company: contact.company.clone(),
24 - title: contact.title.clone(),
25 - tags: contact.tags.clone(),
26 - primary_email: contact.primary_email().map(|s| s.to_string()),
27 - email_count: contact.email_count(),
28 - phone_count: contact.phones.len(),
29 - has_socials: contact.has_social(),
30 - }
31 - }
32 -
33 - #[instrument(skip_all)]
34 - pub(crate) async fn list_contacts_impl(
35 - &self,
36 - params: ListContactsParams,
37 - ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
38 - let contacts = self.state.contacts.list_filtered(
39 - DESKTOP_USER_ID,
40 - params.search.as_deref(),
41 - params.tag.as_deref(),
42 - ).await?;
43 -
44 - if contacts.is_empty() {
45 - return Ok("No contacts found.".to_string());
46 - }
47 - let results: Vec<ContactResult> = contacts.iter().map(Self::contact_to_result).collect();
48 - Ok(serde_json::to_string_pretty(&results)?)
49 - }
50 -
51 - #[instrument(skip_all)]
52 - pub(crate) async fn create_contact_impl(
53 - &self,
54 - params: CreateContactParams,
55 - ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
56 - if params.name.trim().is_empty() {
57 - return Err("Contact name is required".into());
58 - }
59 -
60 - let tags: Vec<String> = params.tags
61 - .as_deref()
62 - .map(|s| s.split(',').map(|t| t.trim().to_string()).filter(|t| !t.is_empty()).collect())
63 - .unwrap_or_default();
64 -
65 - let birthday = match &params.birthday {
66 - Some(s) if !s.is_empty() => Some(NaiveDate::parse_from_str(s, "%Y-%m-%d")?),
67 - _ => None,
68 - };
69 -
70 - let new_contact = NewContact {
71 - display_name: params.name,
72 - nickname: None,
73 - company: params.company,
74 - title: params.title,
75 - notes: params.notes.unwrap_or_default(),
76 - tags,
77 - birthday,
78 - timezone: params.timezone,
79 - };
80 -
81 - let contact = self.state.contacts.create(DESKTOP_USER_ID, new_contact).await?;
82 - let result = Self::contact_to_result(&contact);
83 - Ok(format!("Contact created:\n{}", serde_json::to_string_pretty(&result)?))
84 - }
85 -
86 - #[instrument(skip_all)]
87 - pub(crate) async fn get_contact_impl(
88 - &self,
89 - params: GetContactParams,
90 - ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
91 - let id: ContactId = params.id.parse::<uuid::Uuid>()?.into();
92 - let contact = self.state.contacts.get_by_id(id, DESKTOP_USER_ID).await?
93 - .ok_or_else(|| format!("Contact {} not found", id))?;
94 -
95 - // Full detail response (not just the summary ContactResult)
96 - Ok(serde_json::to_string_pretty(&contact)?)
97 - }
98 -
99 - #[instrument(skip_all)]
100 - pub(crate) async fn update_contact_impl(
101 - &self,
102 - params: UpdateContactParams,
103 - ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
104 - let id: ContactId = params.id.parse::<uuid::Uuid>()?.into();
105 - let existing = self.state.contacts.get_by_id(id, DESKTOP_USER_ID).await?
106 - .ok_or_else(|| format!("Contact {} not found", id))?;
107 -
108 - let display_name = params.name.unwrap_or(existing.display_name);
109 - let company = match &params.company {
110 - Some(s) if s.is_empty() => None,
111 - Some(s) => Some(s.clone()),
112 - None => existing.company,
113 - };
114 - let title = match &params.title {
115 - Some(s) if s.is_empty() => None,
116 - Some(s) => Some(s.clone()),
117 - None => existing.title,
118 - };
119 - let notes = params.notes.unwrap_or(existing.notes);
120 - let tags: Vec<String> = params.tags
121 - .as_deref()
122 - .map(|s| s.split(',').map(|t| t.trim().to_string()).filter(|t| !t.is_empty()).collect())
123 - .unwrap_or(existing.tags);
124 - let birthday = match &params.birthday {
125 - Some(s) if s.is_empty() => None,
126 - Some(s) => Some(NaiveDate::parse_from_str(s, "%Y-%m-%d")?),
127 - None => existing.birthday,
128 - };
129 - let timezone = match &params.timezone {
130 - Some(s) if s.is_empty() => None,
131 - Some(s) => Some(s.clone()),
132 - None => existing.timezone,
133 - };
134 -
135 - let update = UpdateContact {
136 - display_name,
137 - nickname: existing.nickname,
138 - company,
139 - title,
140 - notes,
141 - tags,
142 - birthday,
143 - timezone,
144 - };
145 -
146 - let contact = self.state.contacts.update(id, DESKTOP_USER_ID, update).await?
147 - .ok_or_else(|| format!("Contact {} not found", id))?;
148 - let result = Self::contact_to_result(&contact);
149 - Ok(format!("Contact updated:\n{}", serde_json::to_string_pretty(&result)?))
150 - }
151 -
152 - #[instrument(skip_all)]
153 - pub(crate) async fn delete_contact_impl(
154 - &self,
155 - params: DeleteContactParams,
156 - ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
157 - let id: ContactId = params.id.parse::<uuid::Uuid>()?.into();
158 - let deleted = self.state.contacts.delete(id, DESKTOP_USER_ID).await?;
159 - if deleted {
160 - Ok(format!("Contact {} deleted.", id))
161 - } else {
162 - Ok(format!("Contact {} not found.", id))
163 - }
164 - }
165 -
166 - #[instrument(skip_all)]
167 - pub(crate) async fn add_contact_email_impl(
168 - &self,
169 - params: AddContactEmailParams,
170 - ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
171 - let contact_id: ContactId = params.contact_id.parse::<uuid::Uuid>()?.into();
172 - let new_email = NewContactEmail {
173 - address: params.address,
174 - label: params.label.unwrap_or_default(),
175 - is_primary: params.is_primary.unwrap_or(false),
176 - };
177 - let email = self.state.contacts.add_email(contact_id, DESKTOP_USER_ID, new_email).await?;
178 - Ok(format!("Email added: {} ({})", email.address, email.label))
179 - }
180 -
181 - #[instrument(skip_all)]
182 - pub(crate) async fn remove_contact_email_impl(
183 - &self,
184 - params: RemoveContactSubItemParams,
185 - ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
186 - let id: ContactEmailId = params.id.parse::<uuid::Uuid>()?.into();
187 - let deleted = self.state.contacts.remove_email(id, DESKTOP_USER_ID).await?;
188 - if deleted {
189 - Ok(format!("Email {} removed.", id))
190 - } else {
191 - Ok(format!("Email {} not found.", id))
192 - }
193 - }
194 -
195 - #[instrument(skip_all)]
196 - pub(crate) async fn add_contact_phone_impl(
197 - &self,
198 - params: AddContactPhoneParams,
199 - ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
200 - let contact_id: ContactId = params.contact_id.parse::<uuid::Uuid>()?.into();
201 - let new_phone = NewContactPhone {
202 - number: params.number,
203 - label: params.label.unwrap_or_default(),
204 - is_primary: params.is_primary.unwrap_or(false),
205 - };
206 - let phone = self.state.contacts.add_phone(contact_id, DESKTOP_USER_ID, new_phone).await?;
207 - Ok(format!("Phone added: {} ({})", phone.number, phone.label))
208 - }
209 -
210 - #[instrument(skip_all)]
211 - pub(crate) async fn remove_contact_phone_impl(
212 - &self,
213 - params: RemoveContactSubItemParams,
214 - ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
215 - let id: ContactPhoneId = params.id.parse::<uuid::Uuid>()?.into();
216 - let deleted = self.state.contacts.remove_phone(id, DESKTOP_USER_ID).await?;
217 - if deleted {
218 - Ok(format!("Phone {} removed.", id))
219 - } else {
220 - Ok(format!("Phone {} not found.", id))
221 - }
222 - }
223 -
224 - #[instrument(skip_all)]
225 - pub(crate) async fn add_contact_social_impl(
226 - &self,
227 - params: AddContactSocialParams,
228 - ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
229 - let contact_id: ContactId = params.contact_id.parse::<uuid::Uuid>()?.into();
230 - let new_handle = NewSocialHandle {
231 - platform: params.platform,
232 - handle: params.handle,
233 - url: params.url,
234 - };
235 - let handle = self.state.contacts.add_social_handle(contact_id, DESKTOP_USER_ID, new_handle).await?;
236 - Ok(format!("Social handle added: {} on {}", handle.handle, handle.platform))
237 - }
238 -
239 - #[instrument(skip_all)]
240 - pub(crate) async fn add_contact_custom_field_impl(
241 - &self,
242 - params: AddContactCustomFieldParams,
243 - ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
244 - let contact_id: ContactId = params.contact_id.parse::<uuid::Uuid>()?.into();
245 - let new_field = NewContactCustomField {
246 - label: params.label,
247 - value: params.value,
248 - url: params.url,
249 - };
250 - let field = self.state.contacts.add_custom_field(contact_id, DESKTOP_USER_ID, new_field).await?;
251 - Ok(format!("Custom field added: {}: {}", field.label, field.value))
252 - }
253 -
254 - /// Find a contact by display name (case-insensitive partial match).
255 - /// Used internally by task/event creation to resolve contact names.
256 - pub(crate) async fn find_contact_by_name(
257 - &self,
258 - name: &str,
259 - ) -> Result<Option<goingson_core::Contact>, Box<dyn std::error::Error + Send + Sync>> {
260 - let contacts = self.state.contacts.list_filtered(
261 - DESKTOP_USER_ID,
262 - Some(name),
263 - None,
264 - ).await?;
265 - Ok(contacts.into_iter().next())
266 - }
267 - }
@@ -1,188 +0,0 @@
1 - //! Parameter types for contact MCP tools.
2 -
3 - use schemars::JsonSchema;
4 - use serde::Deserialize;
5 -
6 - /// Parameters for listing contacts
7 - #[derive(Debug, Deserialize, JsonSchema)]
8 - pub struct ListContactsParams {
9 - /// Filter by tag (optional)
10 - #[schemars(description = "Filter by tag (optional, e.g. 'client', 'friend')")]
11 - pub tag: Option<String>,
12 -
13 - /// Search query (optional, matches name, company, notes)
14 - #[schemars(description = "Search contacts by name, company, or notes (optional)")]
15 - pub search: Option<String>,
16 - }
17 -
18 - /// Parameters for creating a contact
19 - #[derive(Debug, Deserialize, JsonSchema)]
20 - pub struct CreateContactParams {
21 - /// Display name (required)
22 - #[schemars(description = "Contact display name")]
23 - pub name: String,
24 -
25 - /// Company (optional)
26 - #[schemars(description = "Company or organization (optional)")]
27 - pub company: Option<String>,
28 -
29 - /// Title/role (optional)
30 - #[schemars(description = "Job title or role (optional)")]
31 - pub title: Option<String>,
32 -
33 - /// Notes (optional)
34 - #[schemars(description = "Free-text notes about the contact (optional)")]
35 - pub notes: Option<String>,
36 -
37 - /// Tags as comma-separated string (optional)
38 - #[schemars(description = "Comma-separated tags, e.g. 'client,engineering'")]
39 - pub tags: Option<String>,
40 -
41 - /// Birthday in YYYY-MM-DD format (optional)
42 - #[schemars(description = "Birthday in YYYY-MM-DD format (optional)")]
43 - pub birthday: Option<String>,
44 -
45 - /// Timezone (optional, IANA format e.g. America/New_York)
46 - #[schemars(description = "Timezone in IANA format, e.g. 'America/New_York' (optional)")]
47 - pub timezone: Option<String>,
48 - }
49 -
50 - /// Parameters for getting a contact
51 - #[derive(Debug, Deserialize, JsonSchema)]
52 - pub struct GetContactParams {
53 - /// Contact ID
54 - #[schemars(description = "Contact ID (UUID)")]
55 - pub id: String,
56 - }
57 -
58 - /// Parameters for updating a contact
59 - #[derive(Debug, Deserialize, JsonSchema)]
60 - pub struct UpdateContactParams {
61 - /// Contact ID to update
62 - #[schemars(description = "Contact ID (UUID) to update")]
63 - pub id: String,
64 -
65 - /// New display name (optional)
66 - #[schemars(description = "New display name (optional)")]
67 - pub name: Option<String>,
68 -
69 - /// New company (optional, empty string clears)
70 - #[schemars(description = "New company (optional, empty string clears)")]
71 - pub company: Option<String>,
72 -
73 - /// New title (optional, empty string clears)
74 - #[schemars(description = "New title (optional, empty string clears)")]
75 - pub title: Option<String>,
76 -
77 - /// New notes (optional)
78 - #[schemars(description = "New notes (optional)")]
79 - pub notes: Option<String>,
80 -
81 - /// New tags as comma-separated string (optional)
82 - #[schemars(description = "New comma-separated tags (optional)")]
83 - pub tags: Option<String>,
84 -
85 - /// New birthday (optional, empty string clears)
86 - #[schemars(description = "New birthday in YYYY-MM-DD format (optional, empty string clears)")]
87 - pub birthday: Option<String>,
88 -
89 - /// New timezone (optional, empty string clears)
90 - #[schemars(description = "New timezone in IANA format (optional, empty string clears)")]
91 - pub timezone: Option<String>,
92 - }
93 -
94 - /// Parameters for deleting a contact
95 - #[derive(Debug, Deserialize, JsonSchema)]
96 - pub struct DeleteContactParams {
97 - /// Contact ID
98 - #[schemars(description = "Contact ID (UUID) to delete")]
99 - pub id: String,
100 - }
101 -
102 - /// Parameters for adding an email to a contact
103 - #[derive(Debug, Deserialize, JsonSchema)]
104 - pub struct AddContactEmailParams {
105 - /// Contact ID
106 - #[schemars(description = "Contact ID (UUID) to add email to")]
107 - pub contact_id: String,
108 -
109 - /// Email address
110 - #[schemars(description = "Email address")]
111 - pub address: String,
112 -
113 - /// Label (e.g. 'work', 'personal')
114 - #[schemars(description = "Label for this email, e.g. 'work', 'personal'")]
115 - pub label: Option<String>,
116 -
117 - /// Whether this is the primary email
118 - #[schemars(description = "Set as primary email (default: false)")]
119 - pub is_primary: Option<bool>,
120 - }
121 -
122 - /// Parameters for adding a phone to a contact
123 - #[derive(Debug, Deserialize, JsonSchema)]
124 - pub struct AddContactPhoneParams {
125 - /// Contact ID
126 - #[schemars(description = "Contact ID (UUID) to add phone to")]
127 - pub contact_id: String,
128 -
129 - /// Phone number
130 - #[schemars(description = "Phone number")]
131 - pub number: String,
132 -
133 - /// Label (e.g. 'mobile', 'work')
134 - #[schemars(description = "Label for this phone, e.g. 'mobile', 'work'")]
135 - pub label: Option<String>,
136 -
137 - /// Whether this is the primary phone
138 - #[schemars(description = "Set as primary phone (default: false)")]
139 - pub is_primary: Option<bool>,
140 - }
141 -
142 - /// Parameters for adding a social handle to a contact
143 - #[derive(Debug, Deserialize, JsonSchema)]
144 - pub struct AddContactSocialParams {
145 - /// Contact ID
146 - #[schemars(description = "Contact ID (UUID) to add social handle to")]
147 - pub contact_id: String,
148 -
149 - /// Platform name (e.g. 'GitHub', 'Twitter', 'LinkedIn')
150 - #[schemars(description = "Platform name, e.g. 'GitHub', 'Twitter', 'LinkedIn'")]
151 - pub platform: String,
152 -
153 - /// Handle/username
154 - #[schemars(description = "Handle or username on the platform")]
155 - pub handle: String,
156 -
157 - /// Profile URL (optional)
158 - #[schemars(description = "Profile URL (optional)")]
159 - pub url: Option<String>,
160 - }
161 -
162 - /// Parameters for adding a custom field to a contact
163 - #[derive(Debug, Deserialize, JsonSchema)]
164 - pub struct AddContactCustomFieldParams {
165 - /// Contact ID
166 - #[schemars(description = "Contact ID (UUID) to add custom field to")]
167 - pub contact_id: String,
168 -
169 - /// Field label
170 - #[schemars(description = "Custom field label, e.g. 'Portfolio', 'Office Hours'")]
171 - pub label: String,
172 -
173 - /// Field value
174 - #[schemars(description = "Custom field value")]
175 - pub value: String,
176 -
177 - /// URL (optional, makes the field a link)
178 - #[schemars(description = "URL associated with the field (optional)")]
179 - pub url: Option<String>,
180 - }
181 -
182 - /// Parameters for removing a sub-collection item by ID
183 - #[derive(Debug, Deserialize, JsonSchema)]
184 - pub struct RemoveContactSubItemParams {
185 - /// Item ID to remove
186 - #[schemars(description = "ID (UUID) of the email, phone, social handle, or custom field to remove")]
187 - pub id: String,
188 - }
@@ -1,243 +0,0 @@
1 - //! Event implementation methods for GoingsOnServer.
2 -
3 - use chrono::{DateTime, Utc};
4 - use tracing::instrument;
5 -
6 - use goingson_core::{BlockType, EventId, NewEvent, ParseableEnum, Recurrence, UpdateEvent};
7 -
8 - use crate::state::DESKTOP_USER_ID;
9 -
10 - use super::GoingsOnServer;
11 - use super::results::EventResult;
12 - use super::event_params::*;
13 -
14 - impl GoingsOnServer {
15 - pub(crate) fn event_to_result(event: &goingson_core::Event) -> EventResult {
16 - EventResult {
17 - id: event.id.to_string(),
18 - title: event.title.clone(),
19 - description: event.description.clone(),
20 - start_time: event.start_time.to_rfc3339(),
21 - end_time: event.end_time.map(|d| d.to_rfc3339()),
22 - location: event.location.clone(),
23 - project_name: event.project_name.clone(),
24 - recurrence: event.recurrence.as_str().to_string(),
25 - }
26 - }
27 -
28 - #[instrument(skip_all)]
29 - pub(crate) async fn create_event_impl(
30 - &self,
31 - params: CreateEventParams,
32 - ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
33 - if params.title.trim().is_empty() {
34 - return Err("Event title is required".into());
35 - }
36 -
37 - let start_time: DateTime<Utc> = DateTime::parse_from_rfc3339(&params.start_time)?.with_timezone(&Utc);
38 - let end_time: Option<DateTime<Utc>> = match &params.end_time {
39 - Some(s) => Some(DateTime::parse_from_rfc3339(s)?.with_timezone(&Utc)),
40 - None => None,
41 - };
42 -
43 - let project_id = match &params.project {
44 - Some(name) => {
45 - let project = self.state.projects.find_by_name(DESKTOP_USER_ID, name).await?;
46 - match project {
47 - Some(p) => Some(p.id),
48 - None => return Err(format!("Project '{}' not found", name).into()),
49 - }
50 - }
51 - None => None,
52 - };
53 -
54 - let recurrence = params.recurrence
55 - .as_deref()
56 - .map(Recurrence::from_str_or_default)
57 - .unwrap_or(Recurrence::None);
58 -
59 - // Look up contact by name
60 - let contact_id = match &params.contact {
61 - Some(name) => {
62 - let contact = self.find_contact_by_name(name).await?;
63 - match contact {
64 - Some(c) => Some(c.id),
65 - None => return Err(format!("Contact '{}' not found", name).into()),
66 - }
67 - }
68 - None => None,
69 - };
70 -
71 - // Parse block type
72 - let block_type = params.block_type
73 - .as_deref()
74 - .and_then(BlockType::from_str_opt);
75 -
76 - let new_event = NewEvent {
77 - user_id: Some(DESKTOP_USER_ID),
78 - project_id,
79 - title: params.title,
80 - description: params.description.unwrap_or_default(),
81 - start_time,
82 - end_time,
83 - location: params.location,
84 - linked_task_id: None,
85 - recurrence,
86 - contact_id,
87 - block_type,
88 - };
89 -
90 - let event = self.state.events.create(DESKTOP_USER_ID, new_event).await?;
91 - let result = Self::event_to_result(&event);
92 - Ok(format!("Event created:\n{}", serde_json::to_string_pretty(&result)?))
93 - }
94 -
95 - #[instrument(skip_all)]
96 - pub(crate) async fn list_events_impl(
97 - &self,
98 - params: ListEventsParams,
99 - ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
100 - let events = if let Some(days) = params.days {
101 - self.state.events.get_upcoming(DESKTOP_USER_ID, days).await?
102 - } else {
103 - self.state.events.list_all(DESKTOP_USER_ID).await?
104 - };
105 -
106 - // Filter by project name if specified
107 - let filtered: Vec<_> = if let Some(ref project_filter) = params.project {
108 - events.into_iter()
109 - .filter(|e| e.project_name.as_ref()
110 - .map(|n| n.to_lowercase().contains(&project_filter.to_lowercase()))
111 - .unwrap_or(false))
112 - .collect()
113 - } else {
114 - events
115 - };
116 -
117 - if filtered.is_empty() {
118 - return Ok("No events found.".to_string());
119 - }
120 - let results: Vec<EventResult> = filtered.iter().map(Self::event_to_result).collect();
121 - Ok(serde_json::to_string_pretty(&results)?)
122 - }
123 -
124 - #[instrument(skip_all)]
125 - pub(crate) async fn list_upcoming_events_impl(
126 - &self,
127 - params: ListUpcomingEventsParams,
128 - ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
129 - let days = params.days.unwrap_or(7);
130 - let events = self.state.events.get_upcoming(DESKTOP_USER_ID, days).await?;
131 - if events.is_empty() {
132 - return Ok(format!("No events in the next {} days.", days));
133 - }
134 - let results: Vec<EventResult> = events.iter().map(Self::event_to_result).collect();
135 - Ok(serde_json::to_string_pretty(&results)?)
136 - }
137 -
138 - #[instrument(skip_all)]
139 - pub(crate) async fn get_event_impl(
140 - &self,
141 - params: GetEventParams,
142 - ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
143 - let id: EventId = params.id.parse::<uuid::Uuid>()?.into();
144 - let event = self.state.events.get_by_id(id, DESKTOP_USER_ID).await?
145 - .ok_or_else(|| format!("Event {} not found", id))?;
146 - let result = Self::event_to_result(&event);
147 - Ok(serde_json::to_string_pretty(&result)?)
148 - }
149 -
150 - #[instrument(skip_all)]
151 - pub(crate) async fn update_event_impl(
152 - &self,
153 - params: UpdateEventParams,
154 - ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
155 - let id: EventId = params.id.parse::<uuid::Uuid>()?.into();
156 - let existing = self.state.events.get_by_id(id, DESKTOP_USER_ID).await?
157 - .ok_or_else(|| format!("Event {} not found", id))?;
158 -
159 - let title = params.title.unwrap_or(existing.title);
160 - let start_time = match &params.start_time {
161 - Some(s) => DateTime::parse_from_rfc3339(s)?.with_timezone(&Utc),
162 - None => existing.start_time,
163 - };
164 - let end_time = match &params.end_time {
165 - Some(s) if s.is_empty() => None,
166 - Some(s) => Some(DateTime::parse_from_rfc3339(s)?.with_timezone(&Utc)),
167 - None => existing.end_time,
168 - };
169 - let description = params.description.unwrap_or(existing.description);
170 - let location = match &params.location {
171 - Some(s) if s.is_empty() => None,
172 - Some(s) => Some(s.clone()),
173 - None => existing.location,
174 - };
175 - let project_id = match &params.project {
176 - Some(name) if name.is_empty() => None,
177 - Some(name) => {
178 - let project = self.state.projects.find_by_name(DESKTOP_USER_ID, name).await?;
179 - match project {
180 - Some(p) => Some(p.id),
181 - None => return Err(format!("Project '{}' not found", name).into()),
182 - }
183 - }
184 - None => existing.project_id,
185 - };
186 - let recurrence = params.recurrence
187 - .as_deref()
188 - .map(Recurrence::from_str_or_default)
189 - .unwrap_or(existing.recurrence);
190 -
191 - // Look up contact (empty string clears)
192 - let contact_id = match &params.contact {
193 - Some(name) if name.is_empty() => None,
194 - Some(name) => {
195 - let contact = self.find_contact_by_name(name).await?;
196 - match contact {
197 - Some(c) => Some(c.id),
198 - None => return Err(format!("Contact '{}' not found", name).into()),
199 - }
200 - }
201 - None => existing.contact_id,
202 - };
203 -
204 - // Parse block type (empty string clears)
205 - let block_type = match &params.block_type {
206 - Some(s) if s.is_empty() => None,
207 - Some(s) => BlockType::from_str_opt(s),
208 - None => existing.block_type,
209 - };
210 -
211 - let updated_event = UpdateEvent {
212 - project_id,
213 - title,
214 - description,
215 - start_time,
216 - end_time,
217 - location,
218 - linked_task_id: existing.linked_task_id,
219 - recurrence,
220 - contact_id,
221 - block_type,
222 - };
223 -
224 - let event = self.state.events.update(id, DESKTOP_USER_ID, updated_event).await?
225 - .ok_or_else(|| format!("Event {} not found", id))?;
226 - let result = Self::event_to_result(&event);
227 - Ok(format!("Event updated:\n{}", serde_json::to_string_pretty(&result)?))
228 - }
229 -
230 - #[instrument(skip_all)]
231 - pub(crate) async fn delete_event_impl(
232 - &self,
233 - params: DeleteEventParams,
234 - ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
235 - let id: EventId = params.id.parse::<uuid::Uuid>()?.into();
236 - let deleted = self.state.events.delete(id, DESKTOP_USER_ID).await?;
237 - if deleted {
238 - Ok(format!("Event {} deleted.", id))
239 - } else {
240 - Ok(format!("Event {} not found.", id))
241 - }
242 - }
243 - }
@@ -1,124 +0,0 @@
1 - //! Parameter types for event MCP tools.
2 -
3 - use schemars::JsonSchema;
4 - use serde::Deserialize;
5 -
6 - /// Parameters for creating an event
7 - #[derive(Debug, Deserialize, JsonSchema)]
8 - pub struct CreateEventParams {
9 - /// Event title
10 - #[schemars(description = "Event title")]
11 - pub title: String,
12 -
13 - /// Start time (ISO 8601)
14 - #[schemars(description = "Start time in ISO 8601 format")]
15 - pub start_time: String,
16 -
17 - /// End time (optional, ISO 8601)
18 - #[schemars(description = "End time in ISO 8601 format (optional)")]
19 - pub end_time: Option<String>,
20 -
21 - /// Description (optional)
22 - #[schemars(description = "Event description (optional)")]
23 - pub description: Option<String>,
24 -
25 - /// Location (optional)
26 - #[schemars(description = "Event location (optional)")]
27 - pub location: Option<String>,
28 -
29 - /// Project name (optional)
30 - #[schemars(description = "Project name to assign event to (optional)")]
31 - pub project: Option<String>,
32 -
33 - /// Recurrence (optional)
34 - #[schemars(description = "Recurrence: Daily, Weekly, Monthly, or None (default: None)")]
35 - pub recurrence: Option<String>,
36 -
37 - /// Contact name to link (optional, partial match)
38 - #[schemars(description = "Contact name to link to event (optional, partial match)")]
39 - pub contact: Option<String>,
40 -
41 - /// Block type (optional: Focus, Personal, Vacation)
42 - #[schemars(description = "Block type: Focus, Personal, or Vacation (optional)")]
43 - pub block_type: Option<String>,
44 - }
45 -
46 - /// Parameters for listing events
47 - #[derive(Debug, Deserialize, JsonSchema)]
48 - pub struct ListEventsParams {
49 - /// Filter by project name (optional)
50 - #[schemars(description = "Filter by project name (optional)")]
51 - pub project: Option<String>,
52 -
53 - /// Limit to upcoming N days (optional)
54 - #[schemars(description = "Only show events in the next N days (optional)")]
55 - pub days: Option<i64>,
56 - }
57 -
58 - /// Parameters for listing upcoming events
59 - #[derive(Debug, Deserialize, JsonSchema)]
60 - pub struct ListUpcomingEventsParams {
61 - /// Number of days ahead (default: 7)
62 - #[schemars(description = "Number of days ahead to look (default: 7)")]
63 - pub days: Option<i64>,
64 - }
65 -
66 - /// Parameters for getting an event
67 - #[derive(Debug, Deserialize, JsonSchema)]
68 - pub struct GetEventParams {
69 - /// Event ID
70 - #[schemars(description = "Event ID (UUID)")]
71 - pub id: String,
72 - }
73 -
74 - /// Parameters for updating an event
75 - #[derive(Debug, Deserialize, JsonSchema)]
76 - pub struct UpdateEventParams {
77 - /// Event ID to update
78 - #[schemars(description = "Event ID (UUID) to update")]
79 - pub id: String,
80 -
81 - /// New title (optional)
82 - #[schemars(description = "New event title (optional)")]
83 - pub title: Option<String>,
84 -
85 - /// New start time (optional, ISO 8601)
86 - #[schemars(description = "New start time in ISO 8601 format (optional)")]
87 - pub start_time: Option<String>,
88 -
89 - /// New end time (optional, ISO 8601, empty string clears)
90 - #[schemars(description = "New end time in ISO 8601 format (optional, empty string clears)")]
91 - pub end_time: Option<String>,
92 -
93 - /// New description (optional)
94 - #[schemars(description = "New event description (optional)")]
95 - pub description: Option<String>,
96 -
97 - /// New location (optional, empty string clears)
98 - #[schemars(description = "New location (optional, empty string clears)")]
99 - pub location: Option<String>,
100 -
101 - /// New project name (optional, empty string clears)
102 - #[schemars(description = "New project name (optional, empty string clears)")]
103 - pub project: Option<String>,
104 -
105 - /// New recurrence (optional)
106 - #[schemars(description = "New recurrence: Daily, Weekly, Monthly, or None (optional)")]
107 - pub recurrence: Option<String>,
108 -
109 - /// New contact name (optional, empty string clears)
110 - #[schemars(description = "New contact name (optional, empty string clears)")]
111 - pub contact: Option<String>,
112 -
113 - /// New block type (optional, empty string clears)
114 - #[schemars(description = "New block type: Focus, Personal, Vacation, or empty string to clear (optional)")]
115 - pub block_type: Option<String>,
116 - }
117 -
118 - /// Parameters for deleting an event
119 - #[derive(Debug, Deserialize, JsonSchema)]
120 - pub struct DeleteEventParams {
121 - /// Event ID
122 - #[schemars(description = "Event ID (UUID) to delete")]
123 - pub id: String,
124 - }
@@ -1,161 +0,0 @@
1 - //! Milestone implementation methods for GoingsOnServer.
2 -
3 - use chrono::NaiveDate;
4 - use tracing::instrument;
5 -
6 - use goingson_core::{MilestoneId, MilestoneStatus, NewMilestone, ParseableEnum, ProjectId, TaskStatus};
7 -
8 - use crate::state::DESKTOP_USER_ID;
9 -
10 - use super::GoingsOnServer;
11 - use super::results::MilestoneResult;
12 - use super::milestone_params::*;
13 -
14 - impl GoingsOnServer {
15 - #[instrument(skip_all)]
16 - pub(crate) async fn list_milestones_impl(
17 - &self,
18 - params: ListMilestonesParams,
19 - ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
20 - let project_id: ProjectId = params.project_id.parse::<uuid::Uuid>()?.into();
21 - let milestones = self.state.milestones.list_by_project(project_id, DESKTOP_USER_ID).await?;
22 -
23 - if milestones.is_empty() {
24 - return Ok("No milestones found for this project.".to_string());
25 - }
26 -
27 - // Get tasks for progress calculation
28 - let all_tasks = self.state.tasks.list_all(DESKTOP_USER_ID).await?;
29 -
30 - let results: Vec<MilestoneResult> = milestones.iter().map(|m| {
31 - let milestone_tasks: Vec<_> = all_tasks.iter()
32 - .filter(|t| t.milestone_id == Some(m.id) && t.status != TaskStatus::Deleted)
33 - .collect();
34 - let task_count = milestone_tasks.len();
35 - let completed_count = milestone_tasks.iter()
36 - .filter(|t| t.status == TaskStatus::Completed)
37 - .count();
38 - let progress = if task_count > 0 {
39 - (completed_count as f32 / task_count as f32) * 100.0
40 - } else {
41 - 0.0
42 - };
43 -
44 - MilestoneResult {
45 - id: m.id.to_string(),
46 - project_id: m.project_id.to_string(),
47 - name: m.name.clone(),
48 - description: m.description.clone(),
49 - position: m.position,
50 - target_date: m.target_date.map(|d| d.to_string()),
51 - status: m.status.as_str().to_string(),
52 - task_count,
53 - completed_count,
54 - progress,
55 - }
56 - }).collect();
57 -
58 - Ok(serde_json::to_string_pretty(&results)?)
59 - }
60 -
61 - #[instrument(skip_all)]
62 - pub(crate) async fn create_milestone_impl(
63 - &self,
64 - params: CreateMilestoneParams,
65 - ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
66 - let project_id: ProjectId = params.project_id.parse::<uuid::Uuid>()?.into();
67 -
68 - if params.name.trim().is_empty() {
69 - return Err("Milestone name is required".into());
70 - }
71 -
72 - let target_date: Option<NaiveDate> = match &params.target_date {
73 - Some(s) => Some(NaiveDate::parse_from_str(s, "%Y-%m-%d")?),
74 - None => None,
75 - };
76 -
77 - // Get current milestones to determine position
78 - let existing = self.state.milestones.list_by_project(project_id, DESKTOP_USER_ID).await?;
79 - let position = existing.len() as i32;
80 -
81 - let new_milestone = NewMilestone {
82 - project_id,
83 - name: params.name,
84 - description: params.description.unwrap_or_default(),
85 - position,
86 - target_date,
87 - };
88 -
89 - let milestone = self.state.milestones.create(DESKTOP_USER_ID, new_milestone).await?;
90 -
91 - let result = MilestoneResult {
92 - id: milestone.id.to_string(),
93 - project_id: milestone.project_id.to_string(),
94 - name: milestone.name,
95 - description: milestone.description,
96 - position: milestone.position,
97 - target_date: milestone.target_date.map(|d| d.to_string()),
98 - status: milestone.status.as_str().to_string(),
99 - task_count: 0,
100 - completed_count: 0,
101 - progress: 0.0,
102 - };
103 -
104 - Ok(format!("Milestone created:\n{}", serde_json::to_string_pretty(&result)?))
105 - }
106 -
107 - #[instrument(skip_all)]
108 - pub(crate) async fn update_milestone_impl(
109 - &self,
110 - params: UpdateMilestoneParams,
111 - ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
112 - let id: MilestoneId = params.id.parse::<uuid::Uuid>()?.into();
113 -
114 - let existing = self.state.milestones.get_by_id(id, DESKTOP_USER_ID).await?
115 - .ok_or_else(|| format!("Milestone {} not found", id))?;
116 -
117 - let name = params.name.unwrap_or(existing.name);
118 - let description = params.description.unwrap_or(existing.description);
119 - let target_date = match &params.target_date {
120 - Some(s) if s.is_empty() => None,
121 - Some(s) => Some(NaiveDate::parse_from_str(s, "%Y-%m-%d")?),
122 - None => existing.target_date,
123 - };
124 - let status = params.status
125 - .as_deref()
126 - .map(MilestoneStatus::from_str_or_default)
127 - .unwrap_or(existing.status);
128 -
129 - let milestone = self.state.milestones.update(id, DESKTOP_USER_ID, &name, &description, target_date, &status).await?
130 - .ok_or_else(|| format!("Milestone {} not found", id))?;
131 -
132 - let result = MilestoneResult {
133 - id: milestone.id.to_string(),
134 - project_id: milestone.project_id.to_string(),
135 - name: milestone.name,
136 - description: milestone.description,
137 - position: milestone.position,
138 - target_date: milestone.target_date.map(|d| d.to_string()),
139 - status: milestone.status.as_str().to_string(),
140 - task_count: 0,
141 - completed_count: 0,
142 - progress: 0.0,
143 - };
144 -
145 - Ok(format!("Milestone updated:\n{}", serde_json::to_string_pretty(&result)?))
146 - }
147 -
148 - #[instrument(skip_all)]
149 - pub(crate) async fn delete_milestone_impl(
150 - &self,
151 - params: DeleteMilestoneParams,
152 - ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
153 - let id: MilestoneId = params.id.parse::<uuid::Uuid>()?.into();
154 - let deleted = self.state.milestones.delete(id, DESKTOP_USER_ID).await?;
155 - if deleted {
156 - Ok(format!("Milestone {} deleted.", id))
157 - } else {
158 - Ok(format!("Milestone {} not found.", id))
159 - }
160 - }
161 - }
@@ -1,64 +0,0 @@
1 - //! Parameter types for milestone MCP tools.
2 -
3 - use schemars::JsonSchema;
4 - use serde::Deserialize;
5 -
6 - /// Parameters for listing milestones
7 - #[derive(Debug, Deserialize, JsonSchema)]
8 - pub struct ListMilestonesParams {
9 - /// Project ID to list milestones for
10 - #[schemars(description = "Project ID (UUID) to list milestones for")]
11 - pub project_id: String,
12 - }
13 -
14 - /// Parameters for creating a milestone
15 - #[derive(Debug, Deserialize, JsonSchema)]
16 - pub struct CreateMilestoneParams {
17 - /// Project ID to create milestone in
18 - #[schemars(description = "Project ID (UUID) to create milestone in")]
19 - pub project_id: String,
20 -
21 - /// Milestone name
22 - #[schemars(description = "Milestone name")]
23 - pub name: String,
24 -
25 - /// Milestone description (optional)
26 - #[schemars(description = "Milestone description (optional)")]
27 - pub description: Option<String>,
28 -
29 - /// Target date in ISO 8601 format (optional)
30 - #[schemars(description = "Target date in ISO 8601 format, e.g. 2026-03-01 (optional)")]
31 - pub target_date: Option<String>,
32 - }
33 -
34 - /// Parameters for updating a milestone
35 - #[derive(Debug, Deserialize, JsonSchema)]
36 - pub struct UpdateMilestoneParams {
37 - /// Milestone ID to update
38 - #[schemars(description = "Milestone ID (UUID) to update")]
39 - pub id: String,
40 -
41 - /// New name (optional)
42 - #[schemars(description = "New milestone name (optional)")]
43 - pub name: Option<String>,
44 -
45 - /// New description (optional)
46 - #[schemars(description = "New milestone description (optional)")]
47 - pub description: Option<String>,
48 -
49 - /// New target date (optional, empty string clears)
50 - #[schemars(description = "New target date in ISO 8601 format (optional, empty string clears)")]
51 - pub target_date: Option<String>,
52 -
53 - /// New status: open or completed (optional)
54 - #[schemars(description = "New status: open or completed (optional)")]
55 - pub status: Option<String>,
56 - }
57 -
58 - /// Parameters for deleting a milestone
59 - #[derive(Debug, Deserialize, JsonSchema)]
60 - pub struct DeleteMilestoneParams {
61 - /// Milestone ID
62 - #[schemars(description = "Milestone ID (UUID) to delete")]
63 - pub id: String,
64 - }
@@ -1,701 +0,0 @@
1 - //! MCP tool definitions for GoingsOn task management.
2 - //!
3 - //! Provides 47 tools for full task, project, event, contact, and dashboard management.
4 - //!
5 - //! This module is split into sub-modules by domain:
6 - //! - `results` - All response/result structs
7 - //! - `task_params` / `task_impl` - Task, annotation, and subtask tools
8 - //! - `project_params` / `project_impl` - Project tools
9 - //! - `event_params` / `event_impl` - Event tools
10 - //! - `milestone_params` / `milestone_impl` - Milestone tools
11 - //! - `contact_params` / `contact_impl` - Contact tools
12 - //! - `utility_params` / `utility_impl` - Search, context, roadmap, dashboard tools
13 -
14 - mod results;
15 - mod task_params;
16 - mod task_impl;
17 - mod project_params;
18 - mod project_impl;
19 - mod event_params;
20 - mod event_impl;
21 - mod milestone_params;
22 - mod milestone_impl;
23 - mod contact_params;
24 - mod contact_impl;
25 - mod utility_params;
26 - mod utility_impl;
27 -
28 - // Re-export param types for external use
29 - pub use task_params::*;
30 - pub use project_params::*;
31 - pub use event_params::*;
32 - pub use milestone_params::*;
33 - pub use contact_params::*;
34 - pub use utility_params::*;
35 -
36 - use rmcp::tool;
37 -
38 - use crate::state::McpState;
39 -
40 - // ============ MCP Server ============
41 -
42 - /// GoingsOn MCP server for task management
43 - #[derive(Clone)]
44 - pub struct GoingsOnServer {
45 - pub(crate) state: McpState,
46 - }
47 -
48 - impl GoingsOnServer {
49 - pub fn new(state: McpState) -> Self {
50 - Self { state }
51 - }
52 - }
53 -
54 - // All #[tool(...)] method definitions must live in this single #[tool(tool_box)] block
55 - // because the rmcp macro requires it. Each method is a thin wrapper that delegates
56 - // to a `_impl` method defined in the corresponding domain `_impl` sub-module.
57 - #[tool(tool_box)]
58 - impl GoingsOnServer {
59 - // --- Task Tools ---
60 -
61 - /// List pending tasks from GoingsOn.
62 - #[tool(description = "List pending tasks from GoingsOn. Returns tasks sorted by urgency. Use this to see what needs to be done.")]
63 - pub async fn list_tasks(
64 - &self,
65 - #[tool(aggr)] params: ListTasksParams,
66 - ) -> String {
67 - match self.list_tasks_impl(params).await {
68 - Ok(result) => result,
69 - Err(e) => format!("Error listing tasks: {}", e),
70 - }
71 - }
72 -
73 - /// Create a new task in GoingsOn.
74 - #[tool(description = "Create a new task in GoingsOn. Provide description and optionally priority (high/medium/low), due date (ISO 8601), project name, tags, contact name, and milestone name.")]
75 - pub async fn create_task(
76 - &self,
77 - #[tool(aggr)] params: CreateTaskParams,
78 - ) -> String {
79 - match self.create_task_impl(params).await {
80 - Ok(result) => result,
81 - Err(e) => format!("Error creating task: {}", e),
82 - }
83 - }
84 -
85 - /// Update an existing task in GoingsOn.
86 - #[tool(description = "Update an existing task in GoingsOn. Provide task ID and fields to update (description, priority, due, project, tags, status, contact, milestone).")]
87 - pub async fn update_task(
88 - &self,
89 - #[tool(aggr)] params: UpdateTaskParams,
90 - ) -> String {
91 - match self.update_task_impl(params).await {
92 - Ok(result) => result,
93 - Err(e) => format!("Error updating task: {}", e),
94 - }
95 - }
96 -
97 - /// Delete a task from GoingsOn.
98 - #[tool(description = "Delete (soft-delete) a task from GoingsOn by its ID.")]
99 - pub async fn delete_task(
100 - &self,
101 - #[tool(aggr)] params: DeleteTaskParams,
102 - ) -> String {
103 - match self.delete_task_impl(params).await {
104 - Ok(result) => result,
105 - Err(e) => format!("Error deleting task: {}", e),
106 - }
107 - }
108 -
109 - /// Mark a task as completed in GoingsOn.
110 - #[tool(description = "Mark a task as completed in GoingsOn by its ID.")]
111 - pub async fn complete_task(
112 - &self,
113 - #[tool(aggr)] params: CompleteTaskParams,
114 - ) -> String {
115 - match self.complete_task_impl(params).await {
116 - Ok(result) => result,
117 - Err(e) => format!("Error completing task: {}", e),
118 - }
119 - }
120 -
121 - /// Snooze a task until a later time.
122 - #[tool(description = "Snooze a task until a later time. Use 'tomorrow', 'next_week', 'weekend', or an ISO 8601 datetime.")]
123 - pub async fn snooze_task(
124 - &self,
125 - #[tool(aggr)] params: SnoozeTaskParams,
126 - ) -> String {
127 - match self.snooze_task_impl(params).await {
128 - Ok(result) => result,
129 - Err(e) => format!("Error snoozing task: {}", e),
130 - }
131 - }
132 -
133 - /// Get a task by ID.
134 - #[tool(description = "Get a task by its ID. Returns full task details including annotations and subtasks.")]
135 - pub async fn get_task(
136 - &self,
137 - #[tool(aggr)] params: GetTaskParams,
138 - ) -> String {
139 - match self.get_task_impl(params).await {
140 - Ok(result) => result,
141 - Err(e) => format!("Error getting task: {}", e),
142 - }
143 - }
144 -
145 - /// Mark a task as started.
146 - #[tool(description = "Mark a task as started (in progress) by its ID.")]
147 - pub async fn start_task(
148 - &self,
149 - #[tool(aggr)] params: StartTaskParams,
150 - ) -> String {
151 - match self.start_task_impl(params).await {
152 - Ok(result) => result,
153 - Err(e) => format!("Error starting task: {}", e),
154 - }
155 - }
156 -
157 - /// List snoozed tasks.
158 - #[tool(description = "List all currently snoozed tasks.")]
159 - pub async fn list_snoozed_tasks(&self) -> String {
160 - match self.list_snoozed_tasks_impl().await {
161 - Ok(result) => result,
162 - Err(e) => format!("Error listing snoozed tasks: {}", e),
163 - }
164 - }
165 -
166 - /// Remove snooze from a task.
167 - #[tool(description = "Remove snooze from a task, making it active again.")]
168 - pub async fn unsnooze_task(
169 - &self,
170 - #[tool(aggr)] params: UnsnoozeTaskParams,
171 - ) -> String {
172 - match self.unsnooze_task_impl(params).await {
173 - Ok(result) => result,
174 - Err(e) => format!("Error unsnoozing task: {}", e),
175 - }
176 - }
177 -
178 - /// List tasks waiting for external response.
179 - #[tool(description = "List all tasks marked as waiting for external response.")]
180 - pub async fn list_waiting_tasks(&self) -> String {
181 - match self.list_waiting_tasks_impl().await {
182 - Ok(result) => result,
183 - Err(e) => format!("Error listing waiting tasks: {}", e),
184 - }
185 - }
186 -
187 - /// Mark a task as waiting for response.
188 - #[tool(description = "Mark a task as waiting for external response. Optionally provide an expected response date.")]
189 - pub async fn mark_task_waiting(
190 - &self,
191 - #[tool(aggr)] params: MarkTaskWaitingParams,
192 - ) -> String {
193 - match self.mark_task_waiting_impl(params).await {
194 - Ok(result) => result,
195 - Err(e) => format!("Error marking task as waiting: {}", e),
196 - }
197 - }
198 -
199 - /// Clear waiting status from a task.
200 - #[tool(description = "Clear the waiting-for-response status from a task.")]
201 - pub async fn clear_task_waiting(
202 - &self,
203 - #[tool(aggr)] params: ClearTaskWaitingParams,
204 - ) -> String {
205 - match self.clear_task_waiting_impl(params).await {
206 - Ok(result) => result,
207 - Err(e) => format!("Error clearing task waiting: {}", e),
208 - }
209 - }
210 -
211 - /// Add an annotation (note) to a task.
212 - #[tool(description = "Add a note/annotation to a task. Use for progress updates, context, or decisions.")]
213 - pub async fn add_annotation(
214 - &self,
215 - #[tool(aggr)] params: AddAnnotationParams,
216 - ) -> String {
217 - match self.add_annotation_impl(params).await {
218 - Ok(result) => result,
219 - Err(e) => format!("Error adding annotation: {}", e),
220 - }
221 - }
222 -
223 - /// List annotations for a task.
224 - #[tool(description = "List all annotations (notes) for a task.")]
225 - pub async fn list_annotations(
226 - &self,
227 - #[tool(aggr)] params: ListAnnotationsParams,
228 - ) -> String {
229 - match self.list_annotations_impl(params).await {
230 - Ok(result) => result,
231 - Err(e) => format!("Error listing annotations: {}", e),
232 - }
233 - }
234 -
235 - /// Delete an annotation.
236 - #[tool(description = "Delete an annotation by its ID.")]
237 - pub async fn delete_annotation(
238 - &self,
239 - #[tool(aggr)] params: DeleteAnnotationParams,
240 - ) -> String {
241 - match self.delete_annotation_impl(params).await {
242 - Ok(result) => result,
243 - Err(e) => format!("Error deleting annotation: {}", e),
244 - }
245 - }
246 -
247 - /// Add a subtask to a task.
248 - #[tool(description = "Add a checklist subtask to a task.")]
249 - pub async fn add_subtask(
250 - &self,
251 - #[tool(aggr)] params: AddSubtaskParams,
252 - ) -> String {
253 - match self.add_subtask_impl(params).await {
254 - Ok(result) => result,
255 - Err(e) => format!("Error adding subtask: {}", e),
256 - }
257 - }
258 -
259 - /// Link a task as a subtask of another task.
260 - #[tool(description = "Link a task as a subtask of another task. Used for multi-phase features where Phase 2 is linked to Phase 1.")]
261 - pub async fn add_subtask_link(
262 - &self,
263 - #[tool(aggr)] params: AddSubtaskLinkParams,
264 - ) -> String {
265 - match self.add_subtask_link_impl(params).await {
266 - Ok(result) => result,
267 - Err(e) => format!("Error linking task: {}", e),
268 - }
269 - }
270 -
271 - /// List subtasks for a task.
272 - #[tool(description = "List all subtasks (checklist items) for a task.")]
273 - pub async fn list_subtasks(
274 - &self,
275 - #[tool(aggr)] params: ListSubtasksParams,
276 - ) -> String {
277 - match self.list_subtasks_impl(params).await {
278 - Ok(result) => result,
279 - Err(e) => format!("Error listing subtasks: {}", e),
280 - }
281 - }
282 -
283 - /// Toggle a subtask's completion status.
284 - #[tool(description = "Toggle a subtask between completed and not completed.")]
285 - pub async fn toggle_subtask(
286 - &self,
287 - #[tool(aggr)] params: ToggleSubtaskParams,
288 - ) -> String {
289 - match self.toggle_subtask_impl(params).await {
290 - Ok(result) => result,
291 - Err(e) => format!("Error toggling subtask: {}", e),
292 - }
293 - }
294 -
295 - /// Delete a subtask.
296 - #[tool(description = "Delete a subtask by its ID.")]
297 - pub async fn delete_subtask(
298 - &self,
299 - #[tool(aggr)] params: DeleteSubtaskParams,
300 - ) -> String {
301 - match self.delete_subtask_impl(params).await {
302 - Ok(result) => result,
303 - Err(e) => format!("Error deleting subtask: {}", e),
304 - }
305 - }
306 -
307 - // --- Project Tools ---
308 -
309 - /// Create a new project in GoingsOn.
310 - #[tool(description = "Create a new project in GoingsOn. Provide a name and optionally description, type (Job/SideProject/Company/Essay/Article/Painting/Other), and status (Active/OnHold/Completed/Archived).")]
311 - pub async fn create_project(
312 - &self,
313 - #[tool(aggr)] params: CreateProjectParams,
314 - ) -> String {
315 - match self.create_project_impl(params).await {
316 - Ok(result) => result,
317 - Err(e) => format!("Error creating project: {}", e),
318 - }
319 - }
320 -
321 - /// List available projects in GoingsOn.
322 - #[tool(description = "List available projects in GoingsOn. Use project names when creating tasks.")]
323 - pub async fn list_projects(&self) -> String {
324 - match self.list_projects_impl().await {
325 - Ok(result) => result,
326 - Err(e) => format!("Error listing projects: {}", e),
327 - }
328 - }
329 -
330 - /// Get a project by ID.
331 - #[tool(description = "Get a project by its ID. Returns project details.")]
332 - pub async fn get_project(
333 - &self,
334 - #[tool(aggr)] params: GetProjectParams,
335 - ) -> String {
336 - match self.get_project_impl(params).await {
337 - Ok(result) => result,
338 - Err(e) => format!("Error getting project: {}", e),
339 - }
340 - }
341 -
342 - /// Update an existing project.
343 - #[tool(description = "Update an existing project. Provide project ID and fields to update (name, description, project_type, status).")]
344 - pub async fn update_project(
345 - &self,
346 - #[tool(aggr)] params: UpdateProjectParams,
347 - ) -> String {
348 - match self.update_project_impl(params).await {
349 - Ok(result) => result,
350 - Err(e) => format!("Error updating project: {}", e),
351 - }
352 - }
353 -
354 - /// Delete a project.
355 - #[tool(description = "Delete a project by its ID.")]
356 - pub async fn delete_project(
357 - &self,
358 - #[tool(aggr)] params: DeleteProjectParams,
359 - ) -> String {
360 - match self.delete_project_impl(params).await {
361 - Ok(result) => result,
362 - Err(e) => format!("Error deleting project: {}", e),
363 - }
364 - }
365 -
366 - // --- Event Tools ---
367 -
368 - /// Create a new event.
369 - #[tool(description = "Create a new calendar event. Provide title, start_time (ISO 8601), and optionally end_time, description, location, project name, recurrence, contact name, and block_type (Focus/Personal/Vacation).")]
370 - pub async fn create_event(
371 - &self,
372 - #[tool(aggr)] params: CreateEventParams,
373 - ) -> String {
374 - match self.create_event_impl(params).await {
375 - Ok(result) => result,
376 - Err(e) => format!("Error creating event: {}", e),
377 - }
378 - }
379 -
380 - /// List events.
381 - #[tool(description = "List events, optionally filtered by project name and/or limited to upcoming N days.")]
382 - pub async fn list_events(
383 - &self,
384 - #[tool(aggr)] params: ListEventsParams,
385 - ) -> String {
386 - match self.list_events_impl(params).await {
387 - Ok(result) => result,
388 - Err(e) => format!("Error listing events: {}", e),
389 - }
390 - }
391 -
392 - /// List upcoming events.
393 - #[tool(description = "List events in the next N days (default: 7). Quick way to see what's coming up.")]
394 - pub async fn list_upcoming_events(
395 - &self,
396 - #[tool(aggr)] params: ListUpcomingEventsParams,
397 - ) -> String {
398 - match self.list_upcoming_events_impl(params).await {
399 - Ok(result) => result,
400 - Err(e) => format!("Error listing upcoming events: {}", e),
401 - }
402 - }
403 -
404 - /// Get an event by ID.
405 - #[tool(description = "Get an event by its ID. Returns full event details.")]
406 - pub async fn get_event(
407 - &self,
408 - #[tool(aggr)] params: GetEventParams,
409 - ) -> String {
410 - match self.get_event_impl(params).await {
411 - Ok(result) => result,
412 - Err(e) => format!("Error getting event: {}", e),
413 - }
414 - }
415 -
416 - /// Update an existing event.
417 - #[tool(description = "Update an existing event. Provide event ID and fields to update (title, start_time, end_time, description, location, project, recurrence, contact, block_type).")]
418 - pub async fn update_event(
419 - &self,
420 - #[tool(aggr)] params: UpdateEventParams,
421 - ) -> String {
422 - match self.update_event_impl(params).await {
423 - Ok(result) => result,
424 - Err(e) => format!("Error updating event: {}", e),
425 - }
426 - }
427 -
428 - /// Delete an event.
429 - #[tool(description = "Delete an event by its ID.")]
430 - pub async fn delete_event(
431 - &self,
432 - #[tool(aggr)] params: DeleteEventParams,
433 - ) -> String {
434 - match self.delete_event_impl(params).await {
435 - Ok(result) => result,
436 - Err(e) => format!("Error deleting event: {}", e),
437 - }
438 - }
439 -
440 - // --- Milestone Tools ---
441 -
442 - /// List milestones for a project.
443 - #[tool(description = "List milestones for a project. Returns milestones with progress tracking.")]
444 - pub async fn list_milestones(
445 - &self,
446 - #[tool(aggr)] params: ListMilestonesParams,
447 - ) -> String {
448 - match self.list_milestones_impl(params).await {
449 - Ok(result) => result,
450 - Err(e) => format!("Error listing milestones: {}", e),
451 - }
452 - }
453 -
454 - /// Create a new milestone.
455 - #[tool(description = "Create a new milestone for a project. Provide project_id, name, and optionally description and target_date.")]
456 - pub async fn create_milestone(
457 - &self,
458 - #[tool(aggr)] params: CreateMilestoneParams,
459 - ) -> String {
460 - match self.create_milestone_impl(params).await {
461 - Ok(result) => result,
462 - Err(e) => format!("Error creating milestone: {}", e),
463 - }
464 - }
465 -
466 - /// Update a milestone.
467 - #[tool(description = "Update an existing milestone. Provide milestone ID and fields to update (name, description, target_date, status).")]
468 - pub async fn update_milestone(
469 - &self,
470 - #[tool(aggr)] params: UpdateMilestoneParams,
471 - ) -> String {
472 - match self.update_milestone_impl(params).await {
473 - Ok(result) => result,
474 - Err(e) => format!("Error updating milestone: {}", e),
475 - }
476 - }
477 -
478 - /// Delete a milestone.
479 - #[tool(description = "Delete a milestone by its ID. Tasks assigned to it will have their milestone cleared.")]
480 - pub async fn delete_milestone(
481 - &self,
482 - #[tool(aggr)] params: DeleteMilestoneParams,
483 - ) -> String {
484 - match self.delete_milestone_impl(params).await {
485 - Ok(result) => result,
486 - Err(e) => format!("Error deleting milestone: {}", e),
487 - }
488 - }
489 -
490 - // --- Contact Tools ---
491 -
492 - /// List contacts.
493 - #[tool(description = "List contacts, optionally filtered by tag or search query (matches name, company, notes).")]
494 - pub async fn list_contacts(
495 - &self,
496 - #[tool(aggr)] params: ListContactsParams,
497 - ) -> String {
498 - match self.list_contacts_impl(params).await {
499 - Ok(result) => result,
500 - Err(e) => format!("Error listing contacts: {}", e),
Lines truncated
@@ -1,143 +0,0 @@
1 - //! Project implementation methods for GoingsOnServer.
2 -
3 - use goingson_core::{NewProject, ParseableEnum, ProjectId, ProjectStatus, ProjectType, UpdateProject};
4 - use tracing::instrument;
5 -
6 - use crate::state::DESKTOP_USER_ID;
7 -
8 - use super::GoingsOnServer;
9 - use super::results::ProjectResult;
10 - use super::project_params::*;
11 -
12 - impl GoingsOnServer {
13 - #[instrument(skip_all)]
14 - pub(crate) async fn create_project_impl(
15 - &self,
16 - params: CreateProjectParams,
17 - ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
18 - if params.name.trim().is_empty() {
19 - return Err("Project name is required".into());
20 - }
21 -
22 - let project_type = params
23 - .project_type
24 - .as_deref()
25 - .map(ProjectType::from_str_or_default)
26 - .unwrap_or(ProjectType::Other);
27 -
28 - let status = params
29 - .status
30 - .as_deref()
31 - .map(ProjectStatus::from_str_or_default)
32 - .unwrap_or(ProjectStatus::Active);
33 -
34 - let new_project = NewProject {
35 - name: params.name,
36 - description: params.description.unwrap_or_default(),
37 - project_type,
38 - status,
39 - };
40 -
41 - let project = self.state.projects.create(DESKTOP_USER_ID, new_project).await?;
42 -
43 - let result = ProjectResult {
44 - id: project.id.to_string(),
45 - name: project.name,
46 - project_type: project.project_type.as_str().to_string(),
47 - status: project.status.as_str().to_string(),
48 - };
49 -
50 - Ok(format!(
51 - "Project created successfully:\n{}",
52 - serde_json::to_string_pretty(&result)?
53 - ))
54 - }
55 -
56 - #[instrument(skip_all)]
57 - pub(crate) async fn list_projects_impl(
58 - &self,
59 - ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
60 - let projects = self.state.projects.list_all(DESKTOP_USER_ID).await?;
61 -
62 - if projects.is_empty() {
63 - return Ok("No projects found.".to_string());
64 - }
65 -
66 - let results: Vec<ProjectResult> = projects
67 - .into_iter()
68 - .map(|p| ProjectResult {
69 - id: p.id.to_string(),
70 - name: p.name,
71 - project_type: p.project_type.as_str().to_string(),
72 - status: p.status.as_str().to_string(),
73 - })
74 - .collect();
75 -
76 - Ok(serde_json::to_string_pretty(&results)?)
77 - }
78 -
79 - #[instrument(skip_all)]
80 - pub(crate) async fn get_project_impl(
81 - &self,
82 - params: GetProjectParams,
83 - ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
84 - let id: ProjectId = params.id.parse::<uuid::Uuid>()?.into();
85 - let project = self.state.projects.get_by_id(id, DESKTOP_USER_ID).await?
86 - .ok_or_else(|| format!("Project {} not found", id))?;
87 -
88 - let result = ProjectResult {
89 - id: project.id.to_string(),
90 - name: project.name,
91 - project_type: project.project_type.as_str().to_string(),
92 - status: project.status.as_str().to_string(),
93 - };
94 - Ok(serde_json::to_string_pretty(&result)?)
95 - }
96 -
97 - #[instrument(skip_all)]
98 - pub(crate) async fn update_project_impl(
99 - &self,
100 - params: UpdateProjectParams,
101 - ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
102 - let id: ProjectId = params.id.parse::<uuid::Uuid>()?.into();
103 - let existing = self.state.projects.get_by_id(id, DESKTOP_USER_ID).await?
104 - .ok_or_else(|| format!("Project {} not found", id))?;
105 -
106 - let name = params.name.unwrap_or(existing.name);
107 - let description = params.description.unwrap_or(existing.description);
108 - let project_type = params.project_type
109 - .as_deref()
110 - .map(ProjectType::from_str_or_default)
111 - .unwrap_or(existing.project_type);
112 - let status = params.status
113 - .as_deref()
114 - .map(ProjectStatus::from_str_or_default)
115 - .unwrap_or(existing.status);
116 -
117 - let update = UpdateProject { name, description, project_type, status };
118 - let project = self.state.projects.update(id, DESKTOP_USER_ID, update).await?
119 - .ok_or_else(|| format!("Project {} not found", id))?;
120 -
121 - let result = ProjectResult {
122 - id: project.id.to_string(),
123 - name: project.name,
124 - project_type: project.project_type.as_str().to_string(),
125 - status: project.status.as_str().to_string(),
126 - };
127 - Ok(format!("Project updated:\n{}", serde_json::to_string_pretty(&result)?))
128 - }
129 -
130 - #[instrument(skip_all)]
131 - pub(crate) async fn delete_project_impl(
132 - &self,
133 - params: DeleteProjectParams,
134 - ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
135 - let id: ProjectId = params.id.parse::<uuid::Uuid>()?.into();
136 - let deleted = self.state.projects.delete(id, DESKTOP_USER_ID).await?;
137 - if deleted {
138 - Ok(format!("Project {} deleted.", id))
139 - } else {
140 - Ok(format!("Project {} not found.", id))
141 - }
142 - }
143 - }