99 serve: Start the MCP server (default)
1010
1111Example:
12- # Start the server
12+ devscontext init
13+ devscontext test --task PROJ-123
1314 devscontext serve
14-
15- # Test with a specific ticket
16- devscontext test --ticket PROJ-123
1715"""
1816
1917from __future__ import annotations
2018
19+ import sys
20+ import time
21+ from pathlib import Path
22+
2123import click
2224
2325from devscontext import __version__
2426
2527
28+ def _success (msg : str ) -> str :
29+ """Format success message with green checkmark."""
30+ return click .style ("✓" , fg = "green" ) + " " + msg
31+
32+
33+ def _error (msg : str ) -> str :
34+ """Format error message with red X."""
35+ return click .style ("✗" , fg = "red" ) + " " + msg
36+
37+
38+ def _info (msg : str ) -> str :
39+ """Format info message with blue arrow."""
40+ return click .style ("→" , fg = "blue" ) + " " + msg
41+
42+
2643@click .group (invoke_without_command = True )
2744@click .version_option (version = __version__ , prog_name = "devscontext" )
45+ @click .option ("--verbose" , "-v" , is_flag = True , help = "Show detailed output" )
2846@click .pass_context
29- def cli (ctx : click .Context ) -> None :
47+ def cli (ctx : click .Context , verbose : bool ) -> None :
3048 """DevsContext - MCP server for AI coding context.
3149
3250 Provides synthesized engineering context from Jira, meeting transcripts,
3351 and local documentation to AI coding assistants.
34-
35- If no command is specified, defaults to 'serve'.
3652 """
53+ ctx .ensure_object (dict )
54+ ctx .obj ["verbose" ] = verbose
55+
3756 if ctx .invoked_subcommand is None :
38- # Default to serve if no command specified
3957 ctx .invoke (serve )
4058
4159
4260@cli .command ()
4361def init () -> None :
44- """Create .devscontext.yaml configuration interactively.
45-
46- Creates a starter configuration file with common defaults.
47- """
48- from pathlib import Path
49-
62+ """Create .devscontext.yaml configuration interactively."""
5063 config_path = Path (".devscontext.yaml" )
64+ gitignore_path = Path (".gitignore" )
5165
5266 if config_path .exists ():
5367 click .echo (f"Config file already exists: { config_path } " )
5468 if not click .confirm ("Overwrite?" , default = False ):
5569 click .echo ("Aborted." )
5670 return
5771
58- config_content = """\
59- # DevsContext Configuration
60- # See https://github.com/anthropics/devscontext for documentation
61-
62- adapters:
63- jira:
64- enabled: true
65- base_url: "https://your-company.atlassian.net"
66- email: "${JIRA_EMAIL}"
67- api_token: "${JIRA_API_TOKEN}"
68-
69- fireflies:
70- enabled: false
71- api_key: "${FIREFLIES_API_KEY}"
72-
73- local_docs:
74- enabled: true
75- paths:
76- - "./docs"
77- - "./CLAUDE.md"
78-
79- synthesis:
80- provider: "anthropic"
81- model: "claude-3-haiku-20240307"
82-
83- cache:
84- ttl_seconds: 300
85- max_size: 100
86- """
72+ click .echo ()
73+ click .echo (click .style ("DevsContext Setup" , bold = True ))
74+ click .echo ()
8775
76+ # Jira configuration
77+ jira_enabled = click .confirm ("Configure Jira?" , default = True )
78+ jira_url = ""
79+ if jira_enabled :
80+ jira_url = click .prompt (
81+ " Jira URL" ,
82+ default = "https://your-company.atlassian.net" ,
83+ )
84+ click .echo (" " + _info ("Set JIRA_EMAIL and JIRA_API_TOKEN environment variables" ))
85+
86+ # Fireflies configuration
87+ click .echo ()
88+ fireflies_enabled = click .confirm ("Configure Fireflies (meeting transcripts)?" , default = False )
89+ if fireflies_enabled :
90+ click .echo (" " + _info ("Set FIREFLIES_API_KEY environment variable" ))
91+
92+ # Local docs configuration
93+ click .echo ()
94+ docs_enabled = click .confirm ("Configure local docs?" , default = True )
95+ docs_paths : list [str ] = []
96+ if docs_enabled :
97+ default_paths = "./docs"
98+ if Path ("CLAUDE.md" ).exists ():
99+ default_paths = "./docs, ./CLAUDE.md"
100+ paths_input = click .prompt (" Doc paths (comma-separated)" , default = default_paths )
101+ docs_paths = [p .strip () for p in paths_input .split ("," ) if p .strip ()]
102+
103+ # Build config
104+ config_lines = [
105+ "# DevsContext Configuration" ,
106+ "" ,
107+ "adapters:" ,
108+ " jira:" ,
109+ f" enabled: { str (jira_enabled ).lower ()} " ,
110+ ]
111+
112+ if jira_enabled :
113+ config_lines .extend (
114+ [
115+ f' base_url: "{ jira_url } "' ,
116+ ' email: "${JIRA_EMAIL}"' ,
117+ ' api_token: "${JIRA_API_TOKEN}"' ,
118+ ]
119+ )
120+
121+ config_lines .extend (
122+ [
123+ "" ,
124+ " fireflies:" ,
125+ f" enabled: { str (fireflies_enabled ).lower ()} " ,
126+ ]
127+ )
128+
129+ if fireflies_enabled :
130+ config_lines .append (' api_key: "${FIREFLIES_API_KEY}"' )
131+
132+ config_lines .extend (
133+ [
134+ "" ,
135+ " local_docs:" ,
136+ f" enabled: { str (docs_enabled ).lower ()} " ,
137+ ]
138+ )
139+
140+ if docs_enabled and docs_paths :
141+ config_lines .append (" paths:" )
142+ for path in docs_paths :
143+ config_lines .append (f' - "{ path } "' )
144+
145+ config_lines .extend (
146+ [
147+ "" ,
148+ "synthesis:" ,
149+ ' provider: "anthropic"' ,
150+ ' model: "claude-3-haiku-20240307"' ,
151+ "" ,
152+ "cache:" ,
153+ " ttl_seconds: 300" ,
154+ " max_size: 100" ,
155+ "" ,
156+ ]
157+ )
158+
159+ config_content = "\n " .join (config_lines )
88160 config_path .write_text (config_content )
89- click .echo (f"Created { config_path } " )
161+
162+ # Add to .gitignore if not already there
163+ gitignore_updated = False
164+ if gitignore_path .exists ():
165+ gitignore_content = gitignore_path .read_text ()
166+ if ".devscontext.yaml" not in gitignore_content :
167+ with gitignore_path .open ("a" ) as f :
168+ if not gitignore_content .endswith ("\n " ):
169+ f .write ("\n " )
170+ f .write ("\n # DevsContext config (contains env var references)\n " )
171+ f .write (".devscontext.yaml\n " )
172+ gitignore_updated = True
173+ else :
174+ gitignore_path .write_text ("# DevsContext config\n .devscontext.yaml\n " )
175+ gitignore_updated = True
176+
177+ # Success message
90178 click .echo ()
91- click .echo ("Next steps:" )
92- click .echo (" 1. Edit .devscontext.yaml with your Jira URL" )
93- click .echo (" 2. Set environment variables:" )
94- click .echo (" export JIRA_EMAIL='your-email@company.com'" )
95- click .echo (" export JIRA_API_TOKEN='your-api-token'" )
96- click .echo (" 3. Test: devscontext test --ticket YOUR-123" )
179+ click .echo (_success (f"Created { config_path } " ))
180+ if gitignore_updated :
181+ click .echo (_success ("Added .devscontext.yaml to .gitignore" ))
182+
183+ click .echo ()
184+ click .echo (click .style ("Next steps:" , bold = True ))
185+ if jira_enabled :
186+ click .echo (" export JIRA_EMAIL='your-email@company.com'" )
187+ click .echo (" export JIRA_API_TOKEN='your-api-token'" )
188+ if fireflies_enabled :
189+ click .echo (" export FIREFLIES_API_KEY='your-api-key'" )
190+ click .echo (" export ANTHROPIC_API_KEY='your-api-key'" )
191+ click .echo ()
192+ click .echo (" devscontext test --task YOUR-123" )
97193
98194
99195@cli .command ()
100- @click .option (
101- "--ticket" ,
102- "-t" ,
103- default = None ,
104- help = "Jira ticket ID to test with (e.g., PROJ-123)" ,
105- )
106- def test (ticket : str | None ) -> None :
107- """Test connection to configured adapters.
108-
109- Verifies that all enabled adapters can connect to their respective
110- services. Optionally tests fetching context for a specific ticket.
111- """
196+ @click .option ("--task" , "-t" , default = None , help = "Jira ticket ID (e.g., PROJ-123)" )
197+ @click .pass_context
198+ def test (ctx : click .Context , task : str | None ) -> None :
199+ """Test connection to configured adapters."""
112200 import asyncio
113201
114202 from devscontext .config import load_devscontext_config
115203 from devscontext .core import DevsContextCore
116204
205+ verbose = ctx .obj .get ("verbose" , False )
206+
117207 try :
118208 config = load_devscontext_config ()
119209 except FileNotFoundError :
120- click .echo ("No .devscontext.yaml found. Run 'devscontext init' first." )
121- return
210+ click .echo (_error ( "No .devscontext.yaml found. Run 'devscontext init' first." ) )
211+ sys . exit ( 1 )
122212
123- click .echo ("Testing adapter connections..." )
213+ click .echo ()
214+ click .echo (click .style ("Connection Status" , bold = True ))
124215 click .echo ()
125216
126217 core = DevsContextCore (config )
@@ -129,47 +220,77 @@ async def run_health_checks() -> dict[str, bool]:
129220 return await core .health_check ()
130221
131222 results = asyncio .run (run_health_checks ())
223+ healthy_count = sum (1 for h in results .values () if h )
132224
133225 for adapter , healthy in results .items ():
134- status = click .style ("✓" , fg = "green" ) if healthy else click .style ("✗" , fg = "red" )
135- click .echo (f" { status } { adapter } " )
226+ if healthy :
227+ click .echo (" " + _success (adapter ))
228+ else :
229+ click .echo (" " + _error (f"{ adapter } (check credentials)" ))
230+
231+ click .echo ()
232+
233+ if not task :
234+ click .echo (_info ("Use --task PROJ-123 to test fetching context" ))
235+ return
136236
237+ if healthy_count == 0 :
238+ click .echo (_error ("No healthy adapters. Fix connections before testing." ))
239+ sys .exit (1 )
240+
241+ click .echo (click .style (f"Fetching context for { task } ..." , bold = True ))
137242 click .echo ()
138243
139- if ticket :
140- click .echo (f"Fetching context for { ticket } ..." )
244+ start_time = time .monotonic ()
141245
142- async def fetch_context () -> str :
143- result = await core .get_task_context (ticket )
144- return result .synthesized
246+ async def fetch_context () -> tuple [ str , list [ str ]] :
247+ result = await core .get_task_context (task )
248+ return result .synthesized , result . sources_used
145249
146- try :
147- output = asyncio .run (fetch_context ())
148- click .echo ()
149- click .echo (output )
150- except Exception as e :
151- click .echo (f"Error: { e } " , err = True )
152- else :
153- click .echo ("Use --ticket PROJ-123 to test fetching a ticket." )
250+ try :
251+ output , sources = asyncio .run (fetch_context ())
252+ duration = time .monotonic () - start_time
253+
254+ click .echo (output )
255+ click .echo ()
256+ click .echo (
257+ click .style (
258+ f"Fetched from { len (sources )} source(s) and synthesized in { duration :.1f} s" ,
259+ fg = "cyan" ,
260+ )
261+ )
262+ except Exception as e :
263+ if verbose :
264+ import traceback
265+
266+ click .echo (traceback .format_exc (), err = True )
267+ click .echo (_error (f"Failed: { e } " ), err = True )
268+ sys .exit (1 )
154269
155270
156271@cli .command ()
157- def serve () -> None :
158- """Start the MCP server (stdio transport).
272+ @click .pass_context
273+ def serve (ctx : click .Context ) -> None :
274+ """Start the MCP server (stdio transport)."""
275+
276+ # Print startup message to stderr (stdout is for MCP protocol)
277+ click .echo (
278+ click .style ("DevsContext" , bold = True ) + " MCP server running" ,
279+ err = True ,
280+ )
281+ click .echo (
282+ "Tools: get_task_context, search_context, get_standards" ,
283+ err = True ,
284+ )
285+ click .echo (err = True )
159286
160- Runs the Model Context Protocol server over stdio, allowing
161- AI coding assistants to connect and request context.
162- """
163287 from devscontext .server import main as server_main
164288
165289 server_main ()
166290
167291
168292def main () -> None :
169- """Main entry point for the CLI.
170-
171- Invokes the click command group.
172- """
293+ """Main entry point for the CLI."""
173294 cli ()
174295
175296
0 commit comments